feat: profile rename subcommand + expand all model display names

This commit is contained in:
2026-05-23 21:13:27 +01:00
parent fb02b165d7
commit 21a86c084f
4 changed files with 440 additions and 653 deletions
+1
View File
@@ -1 +1,2 @@
{"action":"prune","keep":10,"kept":[],"removed":[],"auditedAt":"2026-05-23T19:08:40.613Z"} {"action":"prune","keep":10,"kept":[],"removed":[],"auditedAt":"2026-05-23T19:08:40.613Z"}
{"action":"prune","keep":10,"kept":[],"removed":[],"auditedAt":"2026-05-23T19:55:51.755Z"}
+307 -650
View File
@@ -1,663 +1,320 @@
<!doctype html> <!doctype html>
<html lang="en" class="scroll-smooth"> <html lang="en" class="scroll-smooth">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>harness-sync — One Config, Every Harness</title> <title>harness-sync · one config, every harness</title>
<meta <meta name="description" content="One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine."/>
name="description" <link rel="canonical" href="https://harness-sync.raczylo.com"/>
content="One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine." <meta property="og:type" content="website"/>
/> <meta property="og:url" content="https://harness-sync.raczylo.com"/>
<link rel="canonical" href="https://harness-sync.raczylo.com" /> <meta property="og:title" content="harness-sync · one config, every harness"/>
<meta property="og:type" content="website" /> <meta property="og:description" content="One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine."/>
<meta property="og:url" content="https://harness-sync.raczylo.com" /> <meta property="og:image" content="https://harness-sync.raczylo.com/assets/banner.svg"/>
<meta property="og:title" content="harness-sync — One Config, Every Harness" /> <meta name="twitter:card" content="summary_large_image"/>
<meta property="og:description" content="One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine." /> <meta name="twitter:title" content="harness-sync · one config, every harness"/>
<meta property="og:image" content="https://harness-sync.raczylo.com/assets/banner.svg" /> <meta name="twitter:description" content="One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine."/>
<meta name="twitter:card" content="summary_large_image" /> <script src="https://cdn.tailwindcss.com"></script>
<meta name="twitter:title" content="harness-sync — One Config, Every Harness" /> <script>tailwind.config={darkMode:'class'}</script>
<meta name="twitter:description" content="One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine." /> <link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<script> <style>
tailwind.config = { body{font-family:system-ui,sans-serif}
darkMode: 'class' code,pre,.mono{font-family:"JetBrains Mono",ui-monospace,monospace}
} .terminal{background:#0d1117;color:#c9d1d9;border:1px solid #21262d;border-radius:8px;overflow:hidden}
</script> .terminal-bar{background:#161b22;border-bottom:1px solid #21262d;padding:10px 14px;display:flex;gap:6px}
<link .terminal-dot{width:11px;height:11px;border-radius:50%}
rel="stylesheet" .terminal pre{padding:18px 20px;margin:0;white-space:pre-wrap;font-size:13px;line-height:1.7;overflow-x:auto}
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" .terminal .g{color:#7ee787}.terminal .c{color:#79c0ff}.terminal .y{color:#d29922}.terminal .r{color:#ff7b72}.terminal .w{color:#c9d1d9}.terminal .d{color:#6e7681}
/> section + section{border-top:1px solid theme('colors.neutral.200')}
<link rel="preconnect" href="https://fonts.googleapis.com" /> .dark section + section{border-top:1px solid theme('colors.neutral.800')}
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> @media(max-width:768px){.terminal pre{font-size:12px;padding:14px}}
<link </style>
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" <script>
rel="stylesheet" if(localStorage.theme==="dark"||(!("theme" in localStorage)&&window.matchMedia("(prefers-color-scheme:dark)").matches))document.documentElement.classList.add("dark");else document.documentElement.classList.remove("dark");
/> </script>
<style> </head>
body { font-family: "Inter", sans-serif; } <body class="bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
code, pre { font-family: "JetBrains Mono", monospace; }
.theme-transition {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.animate-fade-in-up { animation: fadeInUp 0.6s ease-out; }
.animate-float { animation: float 3s ease-in-out infinite; }
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .glass {
background: rgba(17, 24, 39, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.gradient-text {
background: linear-gradient(135deg, #10b981 0%, #06b6d4 50%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dark .gradient-text {
background: linear-gradient(135deg, #34d399 0%, #22d3ee 50%, #a78bfa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); }
.dark .shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.4); }
html { scroll-behavior: smooth; }
</style>
<script>
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
</script>
</head>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 theme-transition">
<nav class="fixed w-full glass shadow-modern z-50 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="flex justify-between h-16 items-center">
<a href="#" class="flex items-center hover:opacity-80 transition-opacity duration-300 gap-2">
<i class="fas fa-sync-alt text-2xl text-emerald-500"></i>
<span class="text-xl font-bold gradient-text">harness-sync</span>
</a>
<div class="hidden md:flex space-x-6">
<a href="#problem" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Problem</a>
<a href="#features" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Features</a>
<a href="#harnesses" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Harnesses</a>
<a href="#installation" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Install</a>
<a href="#usage" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Usage</a>
<a href="#architecture" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Architecture</a>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle theme">
<i class="fas fa-moon dark:hidden text-xl"></i>
<i class="fas fa-sun hidden dark:inline text-xl"></i>
</button>
<a href="https://github.com/lukaszraczylo/harness-sync" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="View on GitHub">
<i class="fab fa-github text-xl"></i>
</a>
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle menu">
<i class="fas fa-bars text-xl" id="menu-open-icon"></i>
<i class="fas fa-times text-xl hidden" id="menu-close-icon"></i>
</button>
</div>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
<a href="#problem" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Problem</a>
<a href="#features" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Features</a>
<a href="#harnesses" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Harnesses</a>
<a href="#installation" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Install</a>
<a href="#usage" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Usage</a>
<a href="#architecture" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Architecture</a>
</div>
</div>
</nav>
<!-- Hero --> <!-- Nav -->
<section class="relative pt-24 sm:pt-32 pb-12 sm:pb-20 overflow-hidden"> <nav class="sticky top-0 z-50 bg-white/90 dark:bg-neutral-950/90 backdrop-blur border-b border-neutral-200 dark:border-neutral-800">
<div class="absolute inset-0 bg-gradient-to-br from-emerald-50 via-cyan-50 to-violet-50 dark:from-gray-900 dark:via-emerald-900/20 dark:to-cyan-900/20 theme-transition"></div> <div class="max-w-3xl mx-auto px-5 flex items-center justify-between h-14">
<div class="absolute top-0 -left-4 w-72 h-72 bg-emerald-300 dark:bg-emerald-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float"></div> <a href="#" class="mono text-sm font-medium tracking-tight"><span class="text-emerald-600 dark:text-emerald-400">$</span> harness-sync</a>
<div class="absolute top-0 -right-4 w-72 h-72 bg-cyan-300 dark:bg-cyan-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 1s;"></div> <div class="hidden md:flex items-center gap-6 text-sm text-neutral-500 dark:text-neutral-400">
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-violet-300 dark:bg-violet-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 2s;"></div> <a href="#problem" class="hover:text-neutral-900 dark:hover:text-neutral-100">problem</a>
<a href="#features" class="hover:text-neutral-900 dark:hover:text-neutral-100">features</a>
<a href="#harnesses" class="hover:text-neutral-900 dark:hover:text-neutral-100">harnesses</a>
<a href="#install" class="hover:text-neutral-900 dark:hover:text-neutral-100">install</a>
<a href="#usage" class="hover:text-neutral-900 dark:hover:text-neutral-100">usage</a>
<a href="#architecture" class="hover:text-neutral-900 dark:hover:text-neutral-100">architecture</a>
</div>
<div class="flex items-center gap-3">
<button id="theme-toggle" class="text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 p-1" aria-label="Toggle theme">
<svg id="theme-moon" class="w-4 h-4 dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>
<svg id="theme-sun" class="w-4 h-4 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
</button>
<a href="https://github.com/lukaszraczylo/harness-sync" class="text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 p-1" aria-label="GitHub">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</button>
<button id="mobile-toggle" class="md:hidden text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 p-1" aria-label="Menu">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-950">
<div class="px-5 py-3 flex flex-col gap-2 text-sm">
<a href="#problem" class="text-neutral-500 dark:text-neutral-400 py-1">problem</a>
<a href="#features" class="text-neutral-500 dark:text-neutral-400 py-1">features</a>
<a href="#harnesses" class="text-neutral-500 dark:text-neutral-400 py-1">harnesses</a>
<a href="#install" class="text-neutral-500 dark:text-neutral-400 py-1">install</a>
<a href="#usage" class="text-neutral-500 dark:text-neutral-400 py-1">usage</a>
<a href="#architecture" class="text-neutral-500 dark:text-neutral-400 py-1">architecture</a>
</div>
</div>
</nav>
<div class="relative max-w-6xl mx-auto px-4 sm:px-6"> <!-- Hero -->
<div class="text-center"> <header class="max-w-3xl mx-auto px-5 pt-16 pb-12 md:pt-24 md:pb-16">
<div class="mb-6 sm:mb-8 flex justify-center animate-fade-in-up"> <p class="mono text-sm text-emerald-600 dark:text-emerald-400 mb-4">~ $ harness-sync</p>
<div class="w-24 h-24 sm:w-32 sm:h-32 rounded-2xl bg-gradient-to-br from-emerald-500 via-cyan-500 to-violet-500 flex items-center justify-center shadow-xl animate-float"> <h1 class="text-3xl md:text-4xl font-semibold tracking-tight leading-tight mb-4">One config, every <span class="text-neutral-400 dark:text-neutral-500">LLM</span> harness.</h1>
<i class="fas fa-sync-alt text-white text-4xl sm:text-5xl"></i> <p class="text-lg text-neutral-600 dark:text-neutral-400 mb-8 max-w-xl leading-relaxed">A CLI tool that syncs skills, agents, MCP servers, endpoints, and instructions from a single canonical tree into each harness's native format.</p>
</div> <div class="terminal mb-8">
</div> <div class="terminal-bar">
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4 sm:mb-6 leading-tight animate-fade-in-up" style="animation-delay: 0.1s;"> <span class="terminal-dot" style="background:#ff5f57"></span>
One Config,<br /><span class="gradient-text">Every Harness</span> <span class="terminal-dot" style="background:#febc2e"></span>
</h1> <span class="terminal-dot" style="background:#28c840"></span>
<p class="text-base sm:text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-10 max-w-2xl mx-auto leading-relaxed px-4 animate-fade-in-up" style="animation-delay: 0.2s;"> </div>
One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine. <pre><span class="d"># import from whatever you already use</span>
</p> <span class="g">$</span> <span class="c">harness-sync</span> detect
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center mb-8 sm:mb-12 px-4 animate-fade-in-up" style="animation-delay: 0.3s;"> <span class="w"> ✓ claude-code → ~/.claude/</span>
<a href="#installation" class="group relative bg-gradient-to-r from-emerald-600 to-cyan-600 hover:from-emerald-700 hover:to-cyan-700 text-white px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center shadow-lg hover:shadow-xl hover:scale-105"> <span class="w"> ✓ kilo → ~/.config/kilo/</span>
<span class="relative z-10">Get Started</span> <span class="w"> ✓ crush → ~/.config/crush/</span>
</a> <span class="d"> × goose not found</span>
<a href="https://github.com/lukaszraczylo/harness-sync" class="group glass hover:shadow-lg text-gray-900 dark:text-gray-100 px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center hover:scale-105"> <span class="d"> × opencode not found</span>
<i class="fab fa-github mr-2"></i>View on GitHub
</a>
</div>
<div class="flex flex-wrap justify-center gap-2 sm:gap-4 text-sm px-4 animate-fade-in-up" style="animation-delay: 0.4s;">
<img src="https://img.shields.io/github/v/release/lukaszraczylo/harness-sync" alt="Version" class="h-5" />
<img src="https://img.shields.io/github/license/lukaszraczylo/harness-sync" alt="License" class="h-5" />
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/harness-sync" alt="Go Report" class="h-5" />
</div>
</div>
</div>
</section>
<!-- Problem Section -->
<section id="problem" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">The Problem</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Seven harnesses. Seven copies of the same config. They drift.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-times-circle text-red-500 mr-2"></i>Without harness-sync
</h3>
<div class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<div class="flex items-start gap-2"><i class="fas fa-copy text-red-400 mt-1"></i><span>Same skill markdown, duplicated seven ways</span></div>
<div class="flex items-start gap-2"><i class="fas fa-copy text-red-400 mt-1"></i><span>Same agent definitions, rewritten per harness</span></div>
<div class="flex items-start gap-2"><i class="fas fa-copy text-red-400 mt-1"></i><span>Same MCP registry, seven different key names (<code class="text-xs">mcpServers</code>, <code class="text-xs">mcp</code>, <code class="text-xs">context_servers</code>...)</span></div>
<div class="flex items-start gap-2"><i class="fas fa-copy text-red-400 mt-1"></i><span>Same gateway URL, hand-copied into seven config files</span></div>
<div class="flex items-start gap-2"><i class="fas fa-exclamation-triangle text-red-400 mt-1"></i><span>Edit once. Forget the others. Things break.</span></div>
</div>
</div>
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-check-circle text-emerald-500 mr-2"></i>With harness-sync
</h3>
<div class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<div class="flex items-start gap-2"><i class="fas fa-code-branch text-emerald-400 mt-1"></i><span>One canonical tree under git version control</span></div>
<div class="flex items-start gap-2"><i class="fas fa-cogs text-emerald-400 mt-1"></i><span>Per-harness adapters render native config formats</span></div>
<div class="flex items-start gap-2"><i class="fas fa-random text-emerald-400 mt-1"></i><span>3-way merge via git for conflict resolution</span></div>
<div class="flex items-start gap-2"><i class="fas fa-user-shield text-emerald-400 mt-1"></i><span>Secrets via <code class="text-xs">${VAR}</code> env-var substitution, never stored</span></div>
<div class="flex items-start gap-2"><i class="fas fa-undo text-emerald-400 mt-1"></i><span>Git rollback on every apply</span></div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section --> <span class="g">$</span> <span class="c">harness-sync</span> init
<section id="features" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition"> <span class="w"> imported 12 skills, 4 agents, 6 mcp servers from claude-code</span>
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Features</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Everything you need to manage LLM harness config at scale</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-code-branch text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Canonical Tree</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Profiles, skills, agents, MCP servers, and instructions — one git-managed directory</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-cyan-500 to-cyan-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-exchange-alt text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Native Adapters</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Each harness gets its correct native format — right keys, right types, right paths</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-500 to-violet-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-random text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">3-Way Merge</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Git merge-file semantics — disjoint changes merge cleanly, conflicts produce .rej files</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-layer-group text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Profiles</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Bundle gateway URL, model allowlist, and upstream keys — switch with one command</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-rose-500 to-rose-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-user-shield text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Secret Safety</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Env-var substitution at render time — secrets never stored in canonical tree</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-search text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Auto-Detection</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Detects which harnesses are installed and only touches what's relevant</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-eye text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Dry-Run Preview</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">See exactly what will change before writing — diff every file without touching disk</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-undo text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Git Rollback</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Revert the last N applies — full version history preserved in local git</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-pink-500 to-pink-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-download text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Self-Update</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Pulls latest release in place with SHA-256 verification</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-lime-500 to-lime-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-handshake text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Non-Destructive</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Only overlays managed keys — your hooks, permissions, and editor settings survive</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-file-import text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Import from Any</h3>
<p class="text-sm text-gray-600 dark:text-gray-400"><code class="text-xs">harness-sync init</code> picks the best source harness and imports everything</p>
</div>
</div>
</div>
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-slate-500 to-slate-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-plus-circle text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Extensible</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Add a harness: drop a Go package under <code class="text-xs">internal/adapters/</code></p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Harnesses Section --> <span class="g">$</span> <span class="c">harness-sync</span> apply --dry-run
<section id="harnesses" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition"> <span class="w"> plan: write 14 files to claude-code, 11 to kilo, 9 to crush</span>
<div class="max-w-6xl mx-auto px-4 sm:px-6"> <span class="y"> (dry-run, nothing written)</span>
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Supported Harnesses</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Each adapter grounded against real config files and official docs</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-violet-500 to-violet-700 flex items-center justify-center">
<i class="fas fa-terminal text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">claude-code</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Skills · Instructions</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Built-in subscription — provider config managed by Anthropic directly.</p>
</div>
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-rose-500 to-rose-700 flex items-center justify-center">
<i class="fas fa-fire text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">crush</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Skills · Providers · Models</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Full gateway rendering with provider map and default model support.</p>
</div>
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center">
<i class="fas fa-weight text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">kilo</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Skills · Providers · Models</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Kilo CLI with provider map, model, and small model configuration.</p>
</div>
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-cyan-500 to-cyan-700 flex items-center justify-center">
<i class="fas fa-code text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">opencode</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Skills · Providers · Models · Instructions</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Full rendering with JSONC config support and instructions merging.</p>
</div>
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-500 to-orange-700 flex items-center justify-center">
<i class="fas fa-feather-alt text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">goose</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Skills · Providers · Models</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">YAML config with extensions map and custom provider JSON files.</p>
</div>
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-indigo-700 flex items-center justify-center">
<i class="fas fa-robot text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">cagent</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Providers · Models · Instructions</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Starter YAML config with per-agent model selection.</p>
</div>
<div class="glass p-5 rounded-xl hover:shadow-lg transition-all duration-300 sm:col-span-2 lg:col-span-1">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
<i class="fas fa-pen-nib text-white text-sm"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">zed</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">MCP · Providers · Models</p>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Context servers and language model configuration in settings.json.</p>
</div>
</div>
</div>
</section>
<!-- Installation Section -->
<section id="installation" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Install</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">One binary, zero dependencies, every platform</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
<div class="glass p-6 rounded-xl">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center">
<i class="fas fa-bolt text-white"></i>
</div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">One-liner</h3>
<span class="text-xs bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 px-2 py-0.5 rounded-full">Recommended</span>
</div>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto">
<span class="text-emerald-400">$</span> curl -fsSL https://raw.githubusercontent.com/<br/>&nbsp;&nbsp;lukaszraczylo/harness-sync/main/install.sh | bash
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-3">Picks the right artefact for OS/arch, verifies SHA-256, drops binary in <code>~/.local/bin</code>.</p>
</div>
<div class="glass p-6 rounded-xl">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-amber-700 flex items-center justify-center">
<i class="fas fa-beer-mug-empty text-white"></i>
</div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Homebrew</h3>
</div>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto">
<span class="text-emerald-400">$</span> brew install lukaszraczylo/taps/harness-sync
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-3">macOS and Linux via Homebrew tap.</p>
</div>
<div class="glass p-6 rounded-xl">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
<i class="fas fa-code-branch text-white"></i>
</div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">From Source</h3>
</div>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto">
<span class="text-emerald-400">$</span> git clone https://github.com/<br/>&nbsp;&nbsp;lukaszraczylo/harness-sync<br/>
<span class="text-emerald-400">$</span> make build && make install
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-3">Requires Go 1.24+ and <code>git</code> on <code>$PATH</code>.</p>
</div>
</div>
</div>
</section>
<!-- Usage Section -->
<section id="usage" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Usage</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Three commands to get going</p>
</div>
<div class="grid md:grid-cols-3 gap-6 mb-8">
<div class="glass p-6 rounded-xl">
<div class="flex items-center gap-3 mb-4">
<span class="text-2xl font-bold gradient-text">01</span>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Detect</h3>
</div>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 mb-3">
<span class="text-emerald-400">$</span> harness-sync detect
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Lists which harnesses are installed on your machine.</p>
</div>
<div class="glass p-6 rounded-xl">
<div class="flex items-center gap-3 mb-4">
<span class="text-2xl font-bold gradient-text">02</span>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Init</h3>
</div>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 mb-3">
<span class="text-emerald-400">$</span> harness-sync init
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Imports config from detected harnesses into the canonical tree.</p>
</div>
<div class="glass p-6 rounded-xl">
<div class="flex items-center gap-3 mb-4">
<span class="text-2xl font-bold gradient-text">03</span>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Apply</h3>
</div>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 mb-3">
<span class="text-emerald-400">$</span> harness-sync apply
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">Renders and writes native config to every detected harness.</p>
</div>
</div>
<!-- Commands Reference -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-list text-emerald-500 mr-2"></i>Full Command Reference
</h3>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 sm:p-6 font-mono text-xs sm:text-sm text-gray-100 overflow-x-auto">
<pre class="whitespace-pre-wrap text-gray-300">
<span class="text-emerald-400">$</span> harness-sync detect <span class="text-gray-500"># list adapters + detection status</span>
<span class="text-emerald-400">$</span> harness-sync show [harness...] <span class="text-gray-500"># print files each adapter manages</span>
<span class="text-emerald-400">$</span> harness-sync init <span class="text-gray-500"># import from detected harnesses</span>
<span class="text-gray-500">--from a,b # pick adapters, skip prompt</span>
<span class="text-gray-500">--no-prompt # take all detected</span>
<span class="text-gray-500">--force # allow re-init</span>
<span class="text-emerald-400">$</span> harness-sync apply [harness...] <span class="text-gray-500"># render + write</span>
<span class="text-gray-500">--dry-run # show plan, write nothing</span>
<span class="text-gray-500">--force # overwrite without merge</span>
<span class="text-gray-500">--yes # skip confirmation prompt</span>
<span class="text-emerald-400">$</span> harness-sync diff [harness...] <span class="text-gray-500"># apply --dry-run shorthand</span>
<span class="text-emerald-400">$</span> harness-sync profile list <span class="text-gray-500"># list canonical profiles</span>
<span class="text-emerald-400">$</span> harness-sync profile use &lt;name&gt; <span class="text-gray-500"># switch active profile</span>
<span class="text-gray-500">--apply # reapply after switching</span>
<span class="text-emerald-400">$</span> harness-sync rollback [n] <span class="text-gray-500"># git revert last N applies</span>
<span class="text-emerald-400">$</span> harness-sync update <span class="text-gray-500"># reinstall latest release</span>
</pre> </pre>
</div> </div>
</div> <div class="flex items-center gap-3">
</div> <a href="#install" class="inline-block bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900 px-5 py-2.5 text-sm font-medium rounded">install</a>
</section> <a href="https://github.com/lukaszraczylo/harness-sync" class="inline-block border border-neutral-300 dark:border-neutral-700 px-5 py-2.5 text-sm font-medium rounded hover:bg-neutral-50 dark:hover:bg-neutral-900">github</a>
</div>
</header>
<!-- Architecture Section --> <!-- Problem -->
<section id="architecture" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition"> <section id="problem" class="max-w-3xl mx-auto px-5 py-12 md:py-16">
<div class="max-w-6xl mx-auto px-4 sm:px-6"> <h2 class="text-2xl font-semibold tracking-tight mb-3">The problem</h2>
<div class="text-center mb-8 sm:mb-12"> <p class="text-neutral-600 dark:text-neutral-400 mb-8 leading-relaxed">You run <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">claude-code</code>, <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">kilo</code>, <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">crush</code>, <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">opencode</code>, <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">goose</code>, <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">cagent</code>, <code class="mono text-sm bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded">zed</code> — each keeps its own copy of the same config.</p>
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Architecture</h2> <div class="grid md:grid-cols-2 gap-6">
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Single Go binary, ~5000 LOC, 18 packages, 93 tests</p> <div>
</div> <h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-3">without harness-sync</h3>
<div class="glass p-6 rounded-xl mb-8"> <ul class="space-y-2 text-sm text-neutral-700 dark:text-neutral-300">
<div class="flex justify-center"> <li class="flex gap-2"><span class="text-red-500 shrink-0">✕</span><span>same skill markdown, duplicated seven ways</span></li>
<img src="assets/architecture.svg" alt="harness-sync architecture" class="w-full max-w-3xl" /> <li class="flex gap-2"><span class="text-red-500 shrink-0">✕</span><span>same agent definitions, rewritten per harness</span></li>
</div> <li class="flex gap-2"><span class="text-red-500 shrink-0">✕</span><span>same MCP registry, seven different key names (<code class="mono text-xs">mcpServers</code>, <code class="mono text-xs">mcp</code>, <code class="mono text-xs">context_servers</code>…)</span></li>
</div> <li class="flex gap-2"><span class="text-red-500 shrink-0">✕</span><span>same gateway URL, hand-copied into seven files</span></li>
<div class="grid md:grid-cols-2 gap-6"> <li class="flex gap-2"><span class="text-red-500 shrink-0">✕</span><span>edit once, forget the others, things break</span></li>
<div class="glass p-6 rounded-xl"> </ul>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center"> </div>
<i class="fas fa-folder-tree text-emerald-500 mr-2"></i>Canonical Tree <div>
</h3> <h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-3">with harness-sync</h3>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto"> <ul class="space-y-2 text-sm text-neutral-700 dark:text-neutral-300">
<pre class="text-gray-300"> <li class="flex gap-2"><span class="text-emerald-600 dark:text-emerald-400 shrink-0">→</span><span>one canonical tree under git version control</span></li>
<span class="text-cyan-400">~/.config/harness-sync/</span> <span class="text-gray-500">(git)</span> <li class="flex gap-2"><span class="text-emerald-600 dark:text-emerald-400 shrink-0">→</span><span>per-harness adapters render native config formats</span></li>
├── profiles/<span class="text-amber-300">&lt;name&gt;.yaml</span> <li class="flex gap-2"><span class="text-emerald-600 dark:text-emerald-400 shrink-0">→</span><span>3-way merge via <code class="mono text-xs">git merge-file</code></span></li>
├── skills/<span class="text-amber-300">&lt;name&gt;/SKILL.md</span> <li class="flex gap-2"><span class="text-emerald-600 dark:text-emerald-400 shrink-0">→</span><span>secrets via <code class="mono text-xs">${VAR}</code> env-var substitution, never stored</span></li>
├── agents/<span class="text-amber-300">&lt;name&gt;.md</span> <li class="flex gap-2"><span class="text-emerald-600 dark:text-emerald-400 shrink-0">→</span><span>git rollback on every apply</span></li>
</ul>
</div>
</div>
</section>
<!-- Features -->
<section id="features" class="max-w-3xl mx-auto px-5 py-12 md:py-16">
<h2 class="text-2xl font-semibold tracking-tight mb-6">What it does</h2>
<ul class="space-y-4 text-sm text-neutral-700 dark:text-neutral-300">
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Canonical tree.</strong> Profiles, skills, agents, mcp servers, and instructions in a single git-managed directory.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Native adapters.</strong> Each harness gets its correct keys and file types — no lowest-common-denominator config.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">3-way merge.</strong> <code class="mono text-xs">git merge-file</code> semantics — disjoint changes merge cleanly, conflicts produce <code class="mono text-xs">.rej</code> files.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Profiles.</strong> Bundle gateway URL, model allowlist, and upstream keys — switch with one command.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Secret safety.</strong> Env-var substitution at render time. Secrets never written to disk or canonical tree.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Auto-detection.</strong> Detects installed harnesses. Only touches what's relevant on your machine.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Dry-run preview.</strong> Diff every target file before writing anything to disk.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Git rollback.</strong> Revert the last <em>n</em> applies. Full version history in local git.</div></li>
<li class="flex gap-3"><span class="text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">→</span><div><strong class="font-medium text-neutral-900 dark:text-neutral-100">Non-destructive overlay.</strong> Only managed keys are written. Your hooks, permissions, and editor settings survive.</div></li>
</ul>
</section>
<!-- Harnesses -->
<section id="harnesses" class="max-w-3xl mx-auto px-5 py-12 md:py-16">
<h2 class="text-2xl font-semibold tracking-tight mb-3">Supported harnesses</h2>
<p class="text-neutral-600 dark:text-neutral-400 mb-8 text-sm">Each adapter grounded against real config files and upstream docs.</p>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="border-b border-neutral-200 dark:border-neutral-800 text-left">
<tr class="text-neutral-500 dark:text-neutral-400 mono text-xs uppercase tracking-wide">
<th class="pb-2 pr-4 font-medium">harness</th>
<th class="pb-2 pr-4 font-medium">manages</th>
<th class="pb-2 font-medium">note</th>
</tr>
</thead>
<tbody class="text-neutral-700 dark:text-neutral-300">
<tr class="border-b border-neutral-100 dark:border-neutral-800/60">
<td class="py-2.5 pr-4 mono font-medium">claude-code</td>
<td class="py-2.5 pr-4">mcp · skills · instructions</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">provider config managed by Anthropic directly</td>
</tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60">
<td class="py-2.5 pr-4 mono font-medium">crush</td>
<td class="py-2.5 pr-4">mcp · skills · providers · models</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">full gateway rendering with provider map</td>
</tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60">
<td class="py-2.5 pr-4 mono font-medium">kilo</td>
<td class="py-2.5 pr-4">mcp · skills · providers · models</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">small model + provider map config</td>
</tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60">
<td class="py-2.5 pr-4 mono font-medium">opencode</td>
<td class="py-2.5 pr-4">mcp · skills · providers · models · instructions</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">JSONC config with instructions merging</td>
</tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60">
<td class="py-2.5 pr-4 mono font-medium">goose</td>
<td class="py-2.5 pr-4">mcp · skills · providers · models</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">YAML with extensions map</td>
</tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60">
<td class="py-2.5 pr-4 mono font-medium">cagent</td>
<td class="py-2.5 pr-4">mcp · providers · models · instructions</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">starter YAML with per-agent model</td>
</tr>
<tr>
<td class="py-2.5 pr-4 mono font-medium">zed</td>
<td class="py-2.5 pr-4">mcp · providers · models</td>
<td class="py-2.5 text-neutral-500 dark:text-neutral-500">context servers in settings.json</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Install -->
<section id="install" class="max-w-3xl mx-auto px-5 py-12 md:py-16">
<h2 class="text-2xl font-semibold tracking-tight mb-6">Install</h2>
<div class="space-y-6">
<div>
<h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-2">one-liner (recommended)</h3>
<div class="terminal"><pre><span class="g">$</span> curl -fsSL https://raw.githubusercontent.com/<span class="w">\</span>
lukaszraczylo/harness-sync/main/install.sh | bash</pre></div>
<p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">Picks the right release for OS/arch, verifies SHA-256, drops binary in <code class="mono">~/.local/bin</code>.</p>
</div>
<div>
<h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-2">homebrew</h3>
<div class="terminal"><pre><span class="g">$</span> brew install lukaszraczylo/taps/harness-sync</pre></div>
</div>
<div>
<h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-2">from source</h3>
<div class="terminal"><pre><span class="g">$</span> git clone https://github.com/lukaszraczylo/harness-sync
<span class="g">$</span> make build && make install</pre></div>
<p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">Requires Go 1.24+ and <code class="mono">git</code> on <code class="mono">$PATH</code>.</p>
</div>
</div>
</section>
<!-- Usage -->
<section id="usage" class="max-w-3xl mx-auto px-5 py-12 md:py-16">
<h2 class="text-2xl font-semibold tracking-tight mb-6">Usage</h2>
<div class="terminal mb-8"><pre><span class="d"># 1. see what's installed</span>
<span class="g">$</span> <span class="c">harness-sync</span> detect
<span class="d"># 2. import from detected harnesses into canonical tree</span>
<span class="g">$</span> <span class="c">harness-sync</span> init
<span class="d"># 3. render canonical tree to all harness configs</span>
<span class="g">$</span> <span class="c">harness-sync</span> apply
<span class="g">$</span> <span class="c">harness-sync</span> apply --dry-run <span class="d"># preview without writing</span>
<span class="g">$</span> <span class="c">harness-sync</span> diff <span class="d"># same as apply --dry-run</span>
<span class="g">$</span> <span class="c">harness-sync</span> profile list <span class="d"># list profiles</span>
<span class="g">$</span> <span class="c">harness-sync</span> profile use work <span class="d"># switch active profile</span>
<span class="g">$</span> <span class="c">harness-sync</span> rollback 2 <span class="d"># git revert last 2 applies</span>
<span class="g">$</span> <span class="c">harness-sync</span> update <span class="d"># pull latest release</span>
</pre></div>
<h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-3">flags</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="border-b border-neutral-200 dark:border-neutral-800 text-left">
<tr class="text-neutral-500 dark:text-neutral-400 mono text-xs uppercase tracking-wide"><th class="pb-2 pr-4 font-medium">flag</th><th class="pb-2 font-medium">effect</th></tr>
</thead>
<tbody class="text-neutral-700 dark:text-neutral-300">
<tr class="border-b border-neutral-100 dark:border-neutral-800/60"><td class="py-2 pr-4 mono">--dry-run</td><td class="py-2">show plan, write nothing</td></tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60"><td class="py-2 pr-4 mono">--force</td><td class="py-2">overwrite without merge</td></tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60"><td class="py-2 pr-4 mono">--yes</td><td class="py-2">skip confirmation prompt</td></tr>
<tr class="border-b border-neutral-100 dark:border-neutral-800/60"><td class="py-2 pr-4 mono">--from a,b</td><td class="py-2">pick source adapters for init</td></tr>
<tr><td class="py-2 pr-4 mono">--no-prompt</td><td class="py-2">take all detected sources</td></tr>
</tbody>
</table>
</div>
</section>
<!-- Architecture -->
<section id="architecture" class="max-w-3xl mx-auto px-5 py-12 md:py-16">
<h2 class="text-2xl font-semibold tracking-tight mb-3">Architecture</h2>
<p class="text-neutral-600 dark:text-neutral-400 mb-8 text-sm">Single Go binary. ~7,400 LOC. 174 passing tests. 18 packages.</p>
<div class="flex justify-center mb-8">
<img src="assets/architecture.svg" alt="harness-sync architecture" class="w-full max-w-2xl"/>
</div>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-3">canonical tree</h3>
<div class="terminal"><pre><span class="c">~/.config/harness-sync/</span> <span class="d">(git)</span>
├── profiles/<span class="y">&lt;name&gt;.yaml</span>
├── skills/<span class="y">&lt;name&gt;/SKILL.md</span>
├── agents/<span class="y">&lt;name&gt;.md</span>
├── mcp.yaml ├── mcp.yaml
└── instructions/<span class="text-amber-300">global.md</span> └── instructions/<span class="y">global.md</span></pre></div>
</pre> </div>
</div> <div>
</div> <h3 class="mono text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-3">merge model</h3>
<div class="glass p-6 rounded-xl"> <div class="terminal"><pre><span class="d">base </span> ← state/ <span class="d">(last render)</span>
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center"> <span class="g">ours </span> ← rendered <span class="d">(what we'd write now)</span>
<i class="fas fa-random text-cyan-500 mr-2"></i>3-Way Merge <span class="c">theirs</span> ← on disk <span class="d">(what's actually there)</span>
</h3>
<div class="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto">
<pre class="text-gray-300">
<span class="text-gray-500">base</span> ← state/ <span class="text-gray-500">(last render)</span>
<span class="text-emerald-400">ours</span> ← rendered <span class="text-gray-500">(what we'd write now)</span>
<span class="text-cyan-400">theirs</span> ← on disk <span class="text-gray-500">(what's actually there)</span>
<span class="text-gray-500">disjoint</span> → clean merge, write <span class="d">disjoint</span> → clean merge, write file
<span class="text-amber-300">overlap </span> → .rej file, exit non-zero <span class="y">overlap </span> → .rej file, exit non-zero</pre></div>
</pre> </div>
</div> </div>
</div> </section>
</div>
</div>
</section>
<!-- Footer --> <!-- Footer -->
<footer class="py-8 sm:py-12 bg-gray-900 dark:bg-gray-950 text-gray-400 theme-transition"> <footer class="border-t border-neutral-200 dark:border-neutral-800">
<div class="max-w-6xl mx-auto px-4 sm:px-6"> <div class="max-w-3xl mx-auto px-5 py-8 flex flex-col md:flex-row justify-between items-center gap-3 text-sm text-neutral-500 dark:text-neutral-500">
<div class="flex flex-col md:flex-row justify-between items-center gap-4"> <div class="mono">
<div class="flex items-center gap-2"> <span class="text-emerald-600 dark:text-emerald-400">$</span> harness-sync — one config, every harness
<i class="fas fa-sync-alt text-emerald-500"></i> </div>
<span class="font-semibold text-gray-300">harness-sync</span> <div class="flex items-center gap-5">
<span class="text-sm">— one config, every harness</span> <a href="https://github.com/lukaszraczylo/harness-sync" class="hover:text-neutral-900 dark:hover:text-neutral-100">github</a>
</div> <a href="https://github.com/sponsors/lukaszraczylo" class="hover:text-neutral-900 dark:hover:text-neutral-100">sponsor</a>
<div class="flex items-center gap-6"> <span>MIT</span>
<a href="https://github.com/lukaszraczylo/harness-sync" class="hover:text-gray-200 transition-colors" aria-label="GitHub"> </div>
<i class="fab fa-github text-lg"></i> </div>
</a> </footer>
<a href="https://github.com/sponsors/lukaszraczylo" class="hover:text-gray-200 transition-colors" aria-label="Sponsor">
<i class="fas fa-heart text-lg"></i>
</a>
<span class="text-sm">MIT License</span>
</div>
</div>
</div>
</footer>
<script> <script>
const themeToggle = document.getElementById('theme-toggle'); document.getElementById('theme-toggle').addEventListener('click',()=>{
themeToggle.addEventListener('click', () => { const d=document.documentElement.classList;
const isDark = document.documentElement.classList.contains('dark'); if(d.contains('dark')){d.remove('dark');localStorage.theme='light'}
if (isDark) { else{d.add('dark');localStorage.theme='dark'}
document.documentElement.classList.remove('dark'); });
localStorage.theme = 'light'; const mt=document.getElementById('mobile-toggle'),mm=document.getElementById('mobile-menu');
} else { mt.addEventListener('click',()=>mm.classList.toggle('hidden'));
document.documentElement.classList.add('dark'); mm.querySelectorAll('a').forEach(l=>l.addEventListener('click',()=>mm.classList.add('hidden')));
localStorage.theme = 'dark'; </script>
} </body>
});
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const menuOpenIcon = document.getElementById('menu-open-icon');
const menuCloseIcon = document.getElementById('menu-close-icon');
mobileMenuToggle.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
menuOpenIcon.classList.toggle('hidden');
menuCloseIcon.classList.toggle('hidden');
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
menuOpenIcon.classList.remove('hidden');
menuCloseIcon.classList.add('hidden');
});
});
</script>
</body>
</html> </html>
+69 -3
View File
@@ -34,8 +34,8 @@ func NewProfile(reg *adapter.Registry) *cobra.Command {
} }
names := make([]string, 0, len(entries)) names := make([]string, 0, len(entries))
for _, e := range entries { for _, e := range entries {
if strings.HasSuffix(e.Name(), ".yaml") { if n, ok := strings.CutSuffix(e.Name(), ".yaml"); ok {
names = append(names, strings.TrimSuffix(e.Name(), ".yaml")) names = append(names, n)
} }
} }
sort.Strings(names) sort.Strings(names)
@@ -84,8 +84,54 @@ func NewProfile(reg *adapter.Registry) *cobra.Command {
rootFlag(use) rootFlag(use)
use.Flags().BoolVar(&applyAfter, "apply", false, "run apply automatically after switching profile") use.Flags().BoolVar(&applyAfter, "apply", false, "run apply automatically after switching profile")
rename := &cobra.Command{
Use: "rename <old> <new>",
Short: "Rename a profile (file + name field + active_profile if needed)",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
r, err := resolveRoot(root)
if err != nil {
return err
}
oldName, newName := args[0], args[1]
oldPath := filepath.Join(r, "profiles", oldName+".yaml")
newPath := filepath.Join(r, "profiles", newName+".yaml")
if _, statErr := os.Stat(oldPath); statErr != nil {
return fmt.Errorf("profile %q not found", oldName)
}
if _, statErr := os.Stat(newPath); statErr == nil {
return fmt.Errorf("profile %q already exists", newName)
}
body, err := os.ReadFile(oldPath)
if err != nil {
return err
}
if err := os.WriteFile(newPath, []byte(setProfileName(string(body), newName)), 0o600); err != nil {
return err
}
if err := os.Remove(oldPath); err != nil {
_ = os.Remove(newPath)
return err
}
// Update active_profile in config.yaml if it points at the old name.
configPath := filepath.Join(r, "config.yaml")
if existing, readErr := os.ReadFile(configPath); readErr == nil {
if isActiveProfile(string(existing), oldName) {
_ = os.WriteFile(configPath, []byte(setActiveProfile(string(existing), newName)), 0o600)
}
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Renamed profile %q → %q.\n", oldName, newName)
return nil
},
}
rootFlag(rename)
cmd := &cobra.Command{Use: "profile", Short: "Manage profiles"} cmd := &cobra.Command{Use: "profile", Short: "Manage profiles"}
cmd.AddCommand(list, use) cmd.AddCommand(list, use, rename)
return cmd return cmd
} }
@@ -99,3 +145,23 @@ func setActiveProfile(existing, name string) string {
} }
return strings.TrimRight(existing, "\n") + "\nactive_profile: " + name + "\n" return strings.TrimRight(existing, "\n") + "\nactive_profile: " + name + "\n"
} }
func isActiveProfile(configYAML, name string) bool {
for l := range strings.SplitSeq(configYAML, "\n") {
if rest, ok := strings.CutPrefix(l, "active_profile:"); ok {
return strings.TrimSpace(rest) == name
}
}
return false
}
func setProfileName(body, name string) string {
lines := strings.Split(body, "\n")
for i, l := range lines {
if strings.HasPrefix(l, "name:") {
lines[i] = "name: " + name
return strings.Join(lines, "\n")
}
}
return "name: " + name + "\n" + strings.TrimLeft(body, "\n")
}
+63
View File
@@ -62,6 +62,69 @@ func TestProfileUseHintMessage(t *testing.T) {
assert.Contains(t, buf.String(), "harness-sync apply") assert.Contains(t, buf.String(), "harness-sync apply")
} }
func TestProfileRename(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "old.yaml"), []byte("name: old\ngateway:\n url: https://gw\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: other\n"), 0o600))
var buf bytes.Buffer
cmd := NewProfile(nil)
cmd.SetOut(&buf)
cmd.SetArgs([]string{"rename", "old", "new", "--root", root})
require.NoError(t, cmd.Execute())
// old file gone, new file present with updated name field.
_, err := os.Stat(filepath.Join(root, "profiles", "old.yaml"))
assert.True(t, os.IsNotExist(err), "old.yaml should be removed")
body, err := os.ReadFile(filepath.Join(root, "profiles", "new.yaml"))
require.NoError(t, err)
assert.Contains(t, string(body), "name: new")
assert.NotContains(t, string(body), "name: old")
// config.yaml not modified — old was not active.
cfg, err := os.ReadFile(filepath.Join(root, "config.yaml"))
require.NoError(t, err)
assert.Contains(t, string(cfg), "active_profile: other")
assert.Contains(t, buf.String(), `"old" → "new"`)
}
func TestProfileRenameUpdatesActiveProfile(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "old.yaml"), []byte("name: old\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: old\n"), 0o600))
cmd := NewProfile(nil)
cmd.SetArgs([]string{"rename", "old", "new", "--root", root})
require.NoError(t, cmd.Execute())
cfg, err := os.ReadFile(filepath.Join(root, "config.yaml"))
require.NoError(t, err)
assert.Contains(t, string(cfg), "active_profile: new")
}
func TestProfileRenameErrorNotFound(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
cmd := NewProfile(nil)
cmd.SetArgs([]string{"rename", "missing", "new", "--root", root})
assert.Error(t, cmd.Execute())
}
func TestProfileRenameErrorAlreadyExists(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "old.yaml"), []byte("name: old\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "new.yaml"), []byte("name: new\n"), 0o600))
cmd := NewProfile(nil)
cmd.SetArgs([]string{"rename", "old", "new", "--root", root})
assert.Error(t, cmd.Execute())
}
// TestProfileUseWithApply verifies --apply triggers apply after switching. // TestProfileUseWithApply verifies --apply triggers apply after switching.
func TestProfileUseWithApply(t *testing.T) { func TestProfileUseWithApply(t *testing.T) {
root := t.TempDir() root := t.TempDir()