blob: 64ad60e6cef6daad1409b59452a3f7d7d3e345ad [file] [edit]
import 'dart:io';
import 'package:path/path.dart' as p;
/// Supported agents for skill installation.
///
/// The "generic" agent uses `.agents/skills/`. Antigravity, Codex, and generic are
/// separate CLI options that all map here; only "generic" is stored in
/// skills_config.
enum Agent {
cursor('cursor', '.cursor/skills'),
generic('generic', '.agents/skills'),
claude('claude', '.claude/skills'),
copilot('copilot', '.github/skills'),
cline('cline', '.cline/skills'),
opencode('opencode', '.opencode/skills');
final String cliName;
/// Relative path from project root to the agent's skills/rules directory.
final String skillsRelativePath;
const Agent(this.cliName, this.skillsRelativePath);
/// Aliases that map to this agent in the CLI (e.g. antigravity, codex → generic).
List<String> get cliAliases => switch (this) {
Agent.generic => ['antigravity', 'codex'],
_ => [],
};
/// Returns the absolute skills directory path for this agent within [projectPath].
String skillsPath(String projectPath) =>
p.join(projectPath, skillsRelativePath);
/// Whether this agent is detected in the project at [projectPath].
///
/// Copilot is excluded from auto-detection because its `.github/` marker
/// directory is commonly used for CI/CD and other purposes. Use `--agent
/// copilot` to install explicitly.
bool isDetected(String projectPath) {
return switch (this) {
Agent.cursor => Directory(p.join(projectPath, '.cursor')).existsSync(),
Agent.generic => Directory(p.join(projectPath, '.agents')).existsSync(),
Agent.claude => Directory(p.join(projectPath, '.claude')).existsSync(),
Agent.cline =>
Directory(p.join(projectPath, '.cline')).existsSync() ||
Directory(p.join(projectPath, '.clinerules')).existsSync(),
Agent.copilot => false,
Agent.opencode => Directory(
p.join(projectPath, '.opencode'),
).existsSync(),
};
}
/// Parses a CLI name string into an [Agent].
/// Accepts canonical names and aliases (e.g. antigravity, codex → generic).
static Agent? fromCliName(String name) {
final lower = name.toLowerCase();
for (final agent in Agent.values) {
if (agent.cliName == lower) return agent;
if (agent.cliAliases.contains(lower)) return agent;
}
return null;
}
/// All valid CLI names (canonical + aliases) for --agent and help.
/// Sorted alphabetically with "generic" last.
static List<String> get cliNames {
final all = Agent.values
.expand((e) => [e.cliName, ...e.cliAliases])
.toList();
all.sort((a, b) {
if (a == 'generic') return 1;
if (b == 'generic') return -1;
return a.compareTo(b);
});
return all;
}
/// All valid CLI names as a comma-separated string.
static String get validNames => cliNames.join(', ');
}
/// Detects which agent is being used based on project directory markers.
class AgentDetector {
const AgentDetector();
/// Auto-detects a single agent from project directory markers.
///
/// Returns null if no agent or multiple agents are detected.
Agent? detect(String projectPath) {
final detected = detectAll(projectPath);
if (detected.length == 1) return detected.first;
return null;
}
/// Detects all agents present in the project based on directory markers.
List<Agent> detectAll(String projectPath) {
return Agent.values
.where((agent) => agent.isDetected(projectPath))
.toList();
}
}