blob: 9fe061424b5b0ce0b6c729b6c18af67b341a7690 [file] [edit]
import 'package:args/command_runner.dart';
import 'package:skills/src/core/git_repos.dart';
import '../core/skill_installer.dart';
import '../agent/agent.dart';
import '../models/skill_manifest.dart';
import 'options.dart';
import 'skills_command.dart';
import 'package:skills/src/core/dialog_support.dart';
/// Removes managed skills.
class RemoveCommand extends SkillsCommand {
@override
final String name = 'remove';
@override
final String description = 'Remove managed skills.';
final DialogSupport? _dialogSupport;
RemoveCommand({DialogSupport? dialogSupport})
: _dialogSupport = dialogSupport {
addAgentOption(argParser);
argParser
..addMultiOption(
'package',
abbr: 'p',
help: 'Remove skills for these packages.',
)
..addMultiOption('git', help: 'Remove skills from these git repos only.')
..addMultiOption(
'skill',
abbr: 's',
help: 'Only remove these specific skills.',
)
..addFlag(
'all',
abbr: 'a',
help: 'Remove all managed skills.',
negatable: false,
);
}
@override
Future<void> run() async {
final argResults = this.argResults!;
final workspace = await resolveWorkspace();
final rootPath = workspace.rootPath;
final loaded = await SkillManifest.loadOrEmptyFromRoot(rootPath);
if (loaded.isEmpty) {
logger.info('No managed skills found.');
return;
}
var manifest = loaded;
final packagesToRemove = argResults.multiOption('package').toSet();
final sourcesToRemove = {
...packagesToRemove.map((p) => 'package:$p'),
...argResults
.multiOption('git')
.map((arg) => parseGitRepoArg(arg, usage).cloneUrl),
};
final skillsToRemove = argResults.multiOption('skill').toSet();
final allFlag = argResults.flag('all');
if (skillsToRemove.isNotEmpty && allFlag) {
throw UsageException(
'--all and --skill are mutually exclusive arguments, please provide '
'only one',
usage,
);
}
// Determine which agents to remove from: --agent narrows to one,
// otherwise all agents in the manifest.
final List<Agent> targetAgents;
final parsedAgents = parseAgentOption(argResults);
if (parsedAgents.isNotEmpty) {
targetAgents = parsedAgents;
} else {
targetAgents = manifest.allAgents
.map((name) => Agent.fromCliName(name))
.whereType<Agent>()
.toList();
}
// Prompt the user for the packages to remove if possible and not given.
if (_dialogSupport != null &&
!allFlag &&
sourcesToRemove.isEmpty &&
skillsToRemove.isEmpty) {
final allPackages = {
for (final agent in targetAgents)
...manifest.sourceUrisForAgent(agent.cliName).keys,
}.toList()..sort();
final selectedIndices = await _dialogSupport.showMultiSelectDialog(
allPackages,
title: 'Select sources to remove skills for:',
);
if (selectedIndices != null) {
sourcesToRemove.addAll(selectedIndices.map((i) => allPackages[i]));
} else {
logger.info('Skill removal aborted.');
return;
}
if (sourcesToRemove.isEmpty) {
logger.info('No sources selected for removal.');
return;
}
}
// Prompt the user for skills to remove if possible and not given.
if (_dialogSupport != null && !allFlag && skillsToRemove.isEmpty) {
// All the available skills filtered by selected packages
final potentialSkills = {
for (final agent in targetAgents)
for (final MapEntry(key: sourceUri, value: entry)
in manifest.sourceUrisForAgent(agent.cliName).entries)
if (sourcesToRemove.isEmpty || sourcesToRemove.contains(sourceUri))
...entry.skills.map((skill) => skill.name),
}.toList()..sort();
final selectedIndices = await _dialogSupport.showMultiSelectDialog(
potentialSkills,
title: 'Select skills to remove',
);
if (selectedIndices != null) {
skillsToRemove.addAll(selectedIndices.map((i) => potentialSkills[i]));
} else {
logger.info('Skill removal aborted.');
return;
}
if (skillsToRemove.isEmpty) {
logger.info('No skills selected for removal.');
return;
}
}
// The fully filtered map of things to remove.
final Map</* agent */ String, Map</* Source URI */ String, SkillsEntry>>
filteredSkills = {
for (final agent in targetAgents)
agent.cliName: {
for (final MapEntry(
key: sourceUri,
value: SkillsEntry(skills: skills),
)
in manifest.sourceUrisForAgent(agent.cliName).entries)
if (sourcesToRemove.isEmpty || sourcesToRemove.contains(sourceUri))
sourceUri: SkillsEntry(
skills: [
for (final skill in skills)
if (skillsToRemove.isEmpty ||
skillsToRemove.contains(skill.name))
skill,
],
),
},
};
// If non-interactive and no arguments, list installed skills and exit
if (_dialogSupport == null && skillsToRemove.isEmpty && !allFlag) {
logger.info('Installed skills:');
final installedSkillsAndAgents =
</* Skill name */ String, Set</* Installed agent name */ String>>{};
for (final MapEntry(key: agent, value: packages)
in filteredSkills.entries) {
for (final entry in packages.values) {
for (final skill in entry.skills) {
installedSkillsAndAgents
.putIfAbsent(skill.name, () => {})
.add(agent);
}
}
}
final sortedSkills = installedSkillsAndAgents.keys.toList()..sort();
for (final skillName in sortedSkills) {
final agentsStr = installedSkillsAndAgents[skillName]!.join(', ');
logger.info(' $skillName (installed in: $agentsStr)');
}
logger.info('Rerun with `--skill <name>`, or `--all` to remove skills.');
return;
}
final installer = SkillInstaller(_dialogSupport);
var totalRemoved = 0;
for (final agent in targetAgents) {
final result = await installer.removeSkillsForIde(
agent: agent,
rootPath: rootPath,
manifest: manifest,
sourceUris: sourcesToRemove,
skillNames: skillsToRemove,
);
manifest = result.manifest;
totalRemoved += result.removedCount;
for (final info in result.removed) {
logger.info(' [${info.agentName}] Removed ${info.skillName}');
}
}
await manifest.save(manifestFile(rootPath));
if (manifest.isEmpty) {
await SkillManifest.cleanup(rootPath);
}
logger.info('Removed $totalRemoved skill(s)');
}
}