Auto-detect Install Targets and Add Gemini Sync
Two related improvements to the converter CLI:
Overview
Auto-detect Install Targets and Add Gemini Sync
Overview
Two related improvements to the converter CLI:
install --to all— Auto-detect which AI coding tools are installed and convert to all of them in one commandsync --target gemini— Add Gemini CLI as a sync target (currently missing), then addsync --target allto sync personal config to every detected tool
Problem Statement
Users currently must run 6 separate commands to install to all targets:
bunx @every-env/compound-plugin install compound-engineering --to opencode
bunx @every-env/compound-plugin install compound-engineering --to codex
bunx @every-env/compound-plugin install compound-engineering --to droid
bunx @every-env/compound-plugin install compound-engineering --to cursor
bunx @every-env/compound-plugin install compound-engineering --to pi
bunx @every-env/compound-plugin install compound-engineering --to gemini
Similarly, sync requires separate commands per target. And Gemini sync doesn't exist yet.
Acceptance Criteria
Auto-detect install
- [x]
install --to alldetects installed tools and installs to each - [x]Detection checks config directories and/or binaries for each tool
- [x]Prints which tools were detected and which were skipped
- [x]Tools with no detection signal are skipped (not errored)
- [x]
convert --to allalso works (same detection logic) - [x]Existing
--to <target>behavior unchanged - [x]Tests for detection logic and
alltarget handling
Gemini sync
- [x]
sync --target geminisymlinks skills and writes MCP servers to.gemini/settings.json - [x]MCP servers merged into existing
settings.json(same pattern as writer) - [x]
geminiadded tovalidTargetsinsync.ts - [x]Tests for Gemini sync
Sync all
- [x]
sync --target allsyncs to all detected tools - [x]Reuses same detection logic as install
- [x]Prints summary of what was synced where
Implementation
Phase 1: Tool Detection Utility
Create src/utils/detect-tools.ts
name: string
detected: boolean
reason: string // e.g. "found ~/.codex/" or "not found"
}
const home = os.homedir()
const cwd = process.cwd()
const checks: Array<{ name: string; paths: string[] }> = [
{ name: "opencode", paths: [path.join(home, ".config", "opencode"), path.join(cwd, ".opencode")] },
{ name: "codex", paths: [path.join(home, ".codex")] },
{ name: "droid", paths: [path.join(home, ".factory")] },
{ name: "cursor", paths: [path.join(cwd, ".cursor"), path.join(home, ".cursor")] },
{ name: "pi", paths: [path.join(home, ".pi")] },
{ name: "gemini", paths: [path.join(cwd, ".gemini"), path.join(home, ".gemini")] },
]
const results: DetectedTool[] = []
for (const check of checks) {
let detected = false
let reason = "not found"
for (const p of check.paths) {
if (await pathExists(p)) {
detected = true
reason = `found ${p}`
break
}
}
results.push({ name: check.name, detected, reason })
}
return results
}
const tools = await detectInstalledTools()
return tools.filter((t) => t.detected).map((t) => t.name)
}
Detection heuristics:
| Tool | Check paths | Notes |
|---|---|---|
| OpenCode | ~/.config/opencode/, .opencode/ | XDG config or project-local |
| Codex | ~/.codex/ | Global only |
| Droid | ~/.factory/ | Global only |
| Cursor | .cursor/, ~/.cursor/ | Project-local or global |
| Pi | ~/.pi/ | Global only |
| Gemini | .gemini/, ~/.gemini/ | Project-local or global |
Phase 2: Gemini Sync
Create src/sync/gemini.ts
Follow the Cursor sync pattern (src/sync/cursor.ts) since both use JSON config with mcpServers key:
skills: { name: string; sourceDir: string }[],
mcpServers: Record<string, ClaudeMcpServer>,
outputRoot: string,
): Promise<void> {
const geminiDir = path.join(outputRoot, ".gemini")
// Symlink skills
if (skills.length > 0) {
const skillsDir = path.join(geminiDir, "skills")
await symlinkSkills(skills, skillsDir)
}
// Merge MCP servers into settings.json
if (Object.keys(mcpServers).length > 0) {
const settingsPath = path.join(geminiDir, "settings.json")
let existing: Record<string, unknown> = {}
if (await pathExists(settingsPath)) {
await backupFile(settingsPath)
try {
existing = await readJson<Record<string, unknown>>(settingsPath)
} catch {
console.warn("Warning: existing settings.json could not be parsed and will be replaced.")
}
}
const existingMcp = (existing.mcpServers && typeof existing.mcpServers === "object")
? existing.mcpServers as Record<string, unknown>
: {}
const merged = { ...existing, mcpServers: { ...existingMcp, ...convertMcpServers(mcpServers) } }
await writeJson(settingsPath, merged)
}
}
function convertMcpServers(servers: Record<string, ClaudeMcpServer>) {
const result: Record<string, Record<string, unknown>> = {}
for (const [name, server] of Object.entries(servers)) {
const entry: Record<string, unknown> = {}
if (server.command) {
entry.command = server.command
if (server.args?.length) entry.args = server.args
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
} else if (server.url) {
entry.url = server.url
if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
}
result[name] = entry
}
return result
}
Update src/commands/sync.ts:
- Add
"gemini"tovalidTargetsarray - Import
syncToGeminifrom../sync/gemini - Add case in switch for
"gemini"callingsyncToGemini(skills, mcpServers, outputRoot)
Phase 3: Wire --to all into Install and Convert
Modify src/commands/install.ts:
// In args definition, update --to description:
to: {
type: "string",
default: "opencode",
description: "Target format (opencode | codex | droid | cursor | pi | gemini | all)",
},
// In run(), before the existing target lookup:
if (targetName === "all") {
const detected = await detectInstalledTools()
const activeTargets = detected.filter((t) => t.detected)
if (activeTargets.length === 0) {
console.log("No AI coding tools detected. Install at least one tool first.")
return
}
console.log(`Detected ${activeTargets.length} tools:`)
for (const tool of detected) {
console.log(` ${tool.detected ? "✓" : "✗"} ${tool.name} — ${tool.reason}`)
}
// Install to each detected target
for (const tool of activeTargets) {
const handler = targets[tool.name]
const bundle = handler.convert(plugin, options)
if (!bundle) continue
const root = resolveTargetOutputRoot(tool.name, outputRoot, codexHome, piHome, hasExplicitOutput)
await handler.write(root, bundle)
console.log(`Installed ${plugin.manifest.name} to ${tool.name} at ${root}`)
}
// Codex post-processing
if (activeTargets.some((t) => t.name === "codex")) {
await ensureCodexAgentsFile(codexHome)
}
return
}
Same change in src/commands/convert.ts with its version of resolveTargetOutputRoot.
Phase 4: Wire --target all into Sync
Modify src/commands/sync.ts:
// Update validTargets:
const validTargets = ["opencode", "codex", "pi", "droid", "cursor", "gemini", "all"] as const
// In run(), handle "all":
if (targetName === "all") {
const detected = await detectInstalledTools()
const activeTargets = detected.filter((t) => t.detected).map((t) => t.name)
if (activeTargets.length === 0) {
console.log("No AI coding tools detected.")
return
}
console.log(`Syncing to ${activeTargets.length} detected tools...`)
for (const name of activeTargets) {
// call existing sync logic for each target
}
return
}
Phase 5: Tests
Create tests/detect-tools.test.ts
- Test detection with mocked directories (create temp dirs, check detection)
- Test
getDetectedTargetNamesreturns only detected tools - Test empty detection returns empty array
Create tests/gemini-sync.test.ts
Follow tests/sync-cursor.test.ts pattern:
- Test skills are symlinked to
.gemini/skills/ - Test MCP servers merged into
settings.json - Test existing
settings.jsonis backed up - Test empty skills/servers produce no output
Update tests/cli.test.ts
- Test
--to allflag is accepted - Test
sync --target allis accepted - Test
sync --target geminiis accepted
Phase 6: Documentation
Update README.md:
Add to install section:
# auto-detect installed tools and install to all
bunx @every-env/compound-plugin install compound-engineering --to all
Add to sync section:
# Sync to Gemini
bunx @every-env/compound-plugin sync --target gemini
# Sync to all detected tools
bunx @every-env/compound-plugin sync --target all
What We're NOT Doing
- Not adding binary detection (
which cursor,which gemini) — directory checks are sufficient and don't require shell execution - Not adding interactive prompts ("Install to Cursor? y/n") — auto-detect is fire-and-forget
- Not adding
--excludeflag for skipping specific targets — can use--to X --also Yfor manual selection - Not adding Gemini to the
syncsymlink watcher (no watcher exists for any target)
Complexity Assessment
Low-medium change. All patterns are established:
- Detection utility is new but simple (pathExists checks)
- Gemini sync follows cursor sync pattern exactly
--to allis plumbing — iterate detected tools through existing handlers- No new dependencies needed
References
- Cursor sync (reference pattern):
src/sync/cursor.ts - Gemini writer (merge pattern):
src/targets/gemini.ts - Install command:
src/commands/install.ts - Sync command:
src/commands/sync.ts - File utilities:
src/utils/files.ts - Symlink utilities:
src/utils/symlink.ts
Completion Summary
What Was Delivered
- Tool detection utility (
src/utils/detect-tools.ts) withdetectInstalledTools()andgetDetectedTargetNames() - Gemini sync (
src/sync/gemini.ts) following cursor sync pattern — symlinks skills, merges MCP servers intosettings.json install --to allandconvert --to allauto-detect and install to all detected toolssync --target geminiadded to sync commandsync --target allsyncs to all detected tools with summary output- 8 new tests across 2 test files (detect-tools + sync-gemini)
Implementation Statistics
- 4 new files, 3 modified files
- 139 tests passing (8 new + 131 existing)
- No new dependencies
Git Commits
e4d730dfeat: add detect-tools utility and Gemini sync with testsbc655f7feat: wire --to all into install/convert and --target all/gemini into sync877e265docs: add auto-detect and Gemini sync to README, bump to 0.8.0
Completion Details
- Completed By: Claude Opus 4.6
- Date: 2026-02-14
- Session: Single session, TDD approach