From 00d13ade6c4f03707e221cc6fa2d901412a6058c Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 8 May 2026 19:48:27 +0000 Subject: [PATCH 01/13] Allow selection of packages to install, and support `all` parameter. --- CHANGELOG.md | 1 + lib/src/commands/get_command.dart | 2 +- lib/src/commands/get_skills.dart | 47 +++- lib/src/commands/prune_command.dart | 29 ++- lib/src/commands/remove_command.dart | 4 +- lib/src/commands/skills_command.dart | 10 +- lib/src/core/cli_dialog_support.dart | 13 +- lib/src/core/dialog_support.dart | 9 +- lib/src/core/package_resolver.dart | 12 +- lib/src/core/skill_installer.dart | 37 +--- .../get_command_select_packages_test.dart | 209 ++++++++++++++++++ test/fake_dialog_support.dart | 9 +- .../integration/multi_ide_lifecycle_test.dart | 2 +- 13 files changed, 319 insertions(+), 65 deletions(-) create mode 100644 test/commands/get_command_select_packages_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 0510b40..874cd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.4.0-dev +- feat: Added a dialog to select which packages to install skills from during `skills get`. - refactor: Migrate from `.agent/skills` to `.agents/skills` for the generic IDE adapter. When a `.agent/` dir is detected you will be prompted for what action to take. diff --git a/lib/src/commands/get_command.dart b/lib/src/commands/get_command.dart index 011a1b9..754eda2 100644 --- a/lib/src/commands/get_command.dart +++ b/lib/src/commands/get_command.dart @@ -46,7 +46,7 @@ class GetCommand extends SkillsCommand { dialogSupport: _dialogSupport, gitRunner: _effectiveGitRunner, usage: usage, - packageName: packageNameArg, + packageNames: packageNamesArg?.toSet(), ); } } diff --git a/lib/src/commands/get_skills.dart b/lib/src/commands/get_skills.dart index 6a33198..fab7569 100644 --- a/lib/src/commands/get_skills.dart +++ b/lib/src/commands/get_skills.dart @@ -21,7 +21,7 @@ Future getSkills({ DialogSupport? dialogSupport, GitRunner gitRunner = const GitRunner(), String usage = '', - String? packageName, + Set? packageNames, }) async { final ready = await PubRunner.ensureWorkspaceConfigs(workspace); if (!ready) { @@ -30,11 +30,11 @@ Future getSkills({ final packages = await PackageResolver.resolveWorkspace( workspace, - packageName: packageName, + packageNames: packageNames, ); - if (packageName != null && packages.isEmpty) { - logger.severe('Package "$packageName" not found in dependencies.'); + if (packageNames != null && packages.isEmpty) { + logger.severe('None of the requested packages were found in dependencies.'); return false; } @@ -55,14 +55,49 @@ Future getSkills({ } final resolvedPackageNames = packages.map((p) => p.name).toSet(); - final skills = mergeSkills( + var skills = mergeSkills( dartSkills: dartSkills, registrySkills: registrySkills, resolvedPackageNames: resolvedPackageNames, ); if (skills.isEmpty) { - logger.info('No skills found in ${packageName ?? "any"} packages.'); + logger.info('No skills found in ${packageNames ?? "any"} packages.'); + return false; + } + + if (packageNames == null) { + final packagesWithSkills = + skills.map((skill) => skill.packageName).toSet().toList()..sort(); + if (packagesWithSkills.isNotEmpty) { + if (dialogSupport != null) { + final initialSelected = + Iterable.generate(packagesWithSkills.length).toSet(); + final selectedIndices = await dialogSupport.showMultiSelectDialog( + packagesWithSkills, + title: 'Select packages to install skills from:', + initialSelected: initialSelected, + ); + if (selectedIndices != null) { + final selectedPackages = + selectedIndices.map((i) => packagesWithSkills[i]).toSet(); + skills.removeWhere((s) => !selectedPackages.contains(s.packageName)); + } + } else { + logger.info('Available packages with skills:'); + for (final pkg in packagesWithSkills) { + logger.info(' $pkg'); + } + logger.info( + 'Rerun with trailing arguments for each package you want ' + 'to install skills for, or `all` to install all skills.'); + return false; + } + } + } + + if (skills.isEmpty) { + logger.info('No skills selected to install.'); return false; } diff --git a/lib/src/commands/prune_command.dart b/lib/src/commands/prune_command.dart index 1fa7fb0..2319ae4 100644 --- a/lib/src/commands/prune_command.dart +++ b/lib/src/commands/prune_command.dart @@ -65,21 +65,20 @@ class PruneCommand extends SkillsCommand { for (final ide in targetIdes) { final pkgs = manifest.packagesForIde(ide.cliName); - for (final packageName in pkgs.keys) { - if (referencedNames.contains(packageName)) continue; - - final result = await installer.removeSkillsForIde( - ide: ide, - rootPath: rootPath, - manifest: manifest, - packageName: packageName, - ); - manifest = result.manifest; - totalRemoved += result.removedCount; - prunedPackages.add(packageName); - for (final info in result.removed) { - logger.info(' [${info.ideName}] Removed ${info.skillName}'); - } + final pkgsToPrune = + pkgs.keys.where((name) => !referencedNames.contains(name)).toSet(); + prunedPackages.addAll(pkgsToPrune); + + final result = await installer.removeSkillsForIde( + ide: ide, + rootPath: rootPath, + manifest: manifest, + packageNames: pkgsToPrune, + ); + manifest = result.manifest; + totalRemoved += result.removedCount; + for (final info in result.removed) { + logger.info(' [${info.ideName}] Removed ${info.skillName}'); } } diff --git a/lib/src/commands/remove_command.dart b/lib/src/commands/remove_command.dart index 3d82ed2..6db48d0 100644 --- a/lib/src/commands/remove_command.dart +++ b/lib/src/commands/remove_command.dart @@ -35,7 +35,7 @@ class RemoveCommand extends SkillsCommand { var manifest = loaded; - final packageName = packageNameArg; + final packageName = packageNamesArg; // Determine which IDEs to remove from: --ide narrows to one, // otherwise all IDEs in the manifest. @@ -58,7 +58,7 @@ class RemoveCommand extends SkillsCommand { ide: ide, rootPath: rootPath, manifest: manifest, - packageName: packageName, + packageNames: packageNamesArg?.toSet(), ); manifest = result.manifest; totalRemoved += result.removedCount; diff --git a/lib/src/commands/skills_command.dart b/lib/src/commands/skills_command.dart index d778c53..2753c58 100644 --- a/lib/src/commands/skills_command.dart +++ b/lib/src/commands/skills_command.dart @@ -30,11 +30,11 @@ abstract class SkillsCommand extends Command { return const WorkspaceResolver().resolve(path); } - /// The package name from rest arguments, or null if not specified. - String? get packageNameArg => - argResults != null && argResults!.rest.isNotEmpty - ? argResults!.rest.first - : null; + /// The package names from rest arguments, or null if not specified. + List? get packageNamesArg => switch (argResults?.rest) { + List rest when rest.isNotEmpty => rest, + _ => null, + }; } /// Returns the manifest file for the given [rootPath]. diff --git a/lib/src/core/cli_dialog_support.dart b/lib/src/core/cli_dialog_support.dart index 2c539d5..b95cc71 100644 --- a/lib/src/core/cli_dialog_support.dart +++ b/lib/src/core/cli_dialog_support.dart @@ -26,9 +26,16 @@ class CliUtilDialogSupport implements DialogSupport { } @override - Future?> showMultiSelectDialog(List options, - {String? title}) { + Future?> showMultiSelectDialog( + List options, { + String? title, + Set initialSelected = const {}, + }) { if (title != null) io.stdout.writeln(title); - return cli.showMultiSelectDialog(options, _sharedStdIn); + return cli.showMultiSelectDialog( + options, + _sharedStdIn, + initialSelected: initialSelected, + ); } } diff --git a/lib/src/core/dialog_support.dart b/lib/src/core/dialog_support.dart index 435d732..d353f26 100644 --- a/lib/src/core/dialog_support.dart +++ b/lib/src/core/dialog_support.dart @@ -16,6 +16,11 @@ abstract interface class DialogSupport { /// cancelled or not implemented. /// /// The [title] will be shown in an implementation specific way if given. - Future?> showMultiSelectDialog(List options, - {String? title}); + /// + /// If given, [initialSelected] are the initially selected indices. + Future?> showMultiSelectDialog( + List options, { + String? title, + Set initialSelected = const {}, + }); } diff --git a/lib/src/core/package_resolver.dart b/lib/src/core/package_resolver.dart index 9a7dd75..84b3ace 100644 --- a/lib/src/core/package_resolver.dart +++ b/lib/src/core/package_resolver.dart @@ -65,10 +65,11 @@ class PackageResolver { /// out workspace member packages (those are the user's own code, not /// external dependencies that might ship skills). /// - /// If [packageName] is provided, only that package is returned. + /// If [packageNames] is provided, only those packages are returned. + /// If [packageNames] contains 'all', all packages are returned. static Future> resolveWorkspace( WorkspaceLayout workspace, { - String? packageName, + Set? packageNames, }) async { final memberNames = workspace.packages.map((p) => p.name).toSet(); @@ -90,7 +91,12 @@ class PackageResolver { for (final package in config.packages) { if (memberNames.contains(package.name)) continue; if (seen.contains(package.name)) continue; - if (packageName != null && package.name != packageName) continue; + + if (packageNames != null && + !packageNames.contains('all') && + !packageNames.contains(package.name)) { + continue; + } final rootUri = package.root; if (rootUri.scheme != 'file') continue; diff --git a/lib/src/core/skill_installer.dart b/lib/src/core/skill_installer.dart index 168555e..4ba87da 100644 --- a/lib/src/core/skill_installer.dart +++ b/lib/src/core/skill_installer.dart @@ -126,40 +126,27 @@ class SkillInstaller { } /// Removes skills for [ide] from [manifest]. - /// If [packageName] is set, only that package is removed; otherwise all. + /// + /// If [packageNames] is set, only those packages are removed; otherwise all. + /// If [packageNames] contains `all`, then all packages are also removed. Future removeSkillsForIde({ required Ide ide, required String rootPath, required SkillManifest manifest, - String? packageName, + Set? packageNames, }) async { final adapter = createIdeAdapter(ide, rootPath, _dialogSupport); final removed = []; - if (packageName != null) { - final pkgEntry = manifest.packagesForIde(ide.cliName)[packageName]; - if (pkgEntry == null) { - return SkillRemoveResult( - manifest: manifest, - removedCount: 0, - removed: [], - ); - } - for (final skill in pkgEntry.skills) { - await adapter.removeSkill(skill.name); - removed.add( - RemovedSkillInfo(ideName: ide.cliName, skillName: skill.name), - ); - } - return SkillRemoveResult( - manifest: manifest.withoutPackage(ide.cliName, packageName), - removedCount: removed.length, - removed: removed, - ); - } - final pkgs = manifest.packagesForIde(ide.cliName); + for (final entry in pkgs.entries) { + if (packageNames != null && + !packageNames.contains('all') && + !packageNames.contains(entry.key)) { + continue; + } + manifest = manifest.withoutPackage(ide.cliName, entry.key); for (final skill in entry.value.skills) { await adapter.removeSkill(skill.name); removed.add( @@ -168,7 +155,7 @@ class SkillInstaller { } } return SkillRemoveResult( - manifest: manifest.withoutIde(ide.cliName), + manifest: manifest, removedCount: removed.length, removed: removed, ); diff --git a/test/commands/get_command_select_packages_test.dart b/test/commands/get_command_select_packages_test.dart new file mode 100644 index 0000000..863c976 --- /dev/null +++ b/test/commands/get_command_select_packages_test.dart @@ -0,0 +1,209 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart' as p; +import 'package:skills/src/commands/get_command.dart'; +import 'package:skills/src/core/git_runner.dart'; +import 'package:skills/src/ide/adapters/agent_skills_adapter.dart'; +import 'package:skills/src/ide/adapters/generic_adapter.dart'; +import 'package:skills/src/ide/ide.dart'; +import 'package:skills/src/models/skill_manifest.dart'; +import '../fake_dialog_support.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + group('Given a project with dependencies dep1 and dep2 having skills', () { + late String projectPath; + late FakeDialogSupport fakeDialogSupport; + late AgentSkillsAdapter skillsAdapter; + + setUp(() async { + final dep1Dir = d.dir('dep1', [ + d.dir('lib', [d.file('dep1.dart', '')]), + d.dir('skills', [ + d.dir('dep1-skill', [ + d.file('SKILL.md', '---\nname: dep1-skill\n---\n'), + ]), + ]), + ]); + await dep1Dir.create(); + + final dep2Dir = d.dir('dep2', [ + d.dir('lib', [d.file('dep2.dart', '')]), + d.dir('skills', [ + d.dir('dep2-skill', [ + d.file('SKILL.md', '---\nname: dep2-skill\n---\n'), + ]), + ]), + ]); + await dep2Dir.create(); + + final projectRootDir = d.dir('project', [ + d.file('pubspec.yaml', ''' +name: test_app +environment: + sdk: ^3.0.0 +'''), + d.dir('.dart_tool', [ + d.file( + 'package_config.json', + jsonEncode({ + 'configVersion': 2, + 'packages': [ + {'name': 'test_app', 'rootUri': '../', 'packageUri': 'lib/'}, + { + 'name': 'dep1', + 'rootUri': dep1Dir.io.path, + 'packageUri': 'lib/' + }, + { + 'name': 'dep2', + 'rootUri': dep2Dir.io.path, + 'packageUri': 'lib/' + }, + ], + }), + ), + ]), + d.dir('.cursor', [d.dir('skills')]), + ]); + await projectRootDir.create(); + + projectPath = projectRootDir.io.path; + fakeDialogSupport = FakeDialogSupport(); + skillsAdapter = GenericAdapter(projectPath, fakeDialogSupport); + }); + + test('when running `skills get` and the user only selects dep1', () async { + fakeDialogSupport.multiSelectResult = {0}; + final getCommand = GetCommand( + dialogSupport: fakeDialogSupport, + gitRunner: + GitRunner(isAvailableOverride: () async => false), // skip registry + ); + final runner = CommandRunner('skills', 'Test') + ..addCommand(getCommand); + + await runner.run( + ['get', '--directory', projectPath, '--ide', Ide.generic.cliName]); + + expect(fakeDialogSupport.lastInitialSelected, equals({0, 1}), + reason: 'then all packages should be selected by default'); + + final dep1SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep1-skill'), + ); + final dep2SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep2-skill'), + ); + + expect(await dep1SkillDir.exists(), isTrue, + reason: 'then dep1 skill should be installed'); + expect(await dep2SkillDir.exists(), isFalse, + reason: 'then dep2 skill should NOT be installed'); + + final manifestFile = File(SkillManifest.pathIn(projectPath)); + expect(await manifestFile.exists(), isTrue); + + final manifest = await SkillManifest.loadOrEmpty(manifestFile); + final skillNames = manifest + .allSkillsForIde(Ide.generic.cliName) + .map((e) => e.name) + .toSet(); + expect(skillNames, contains('dep1-skill'), + reason: 'then manifest should contain dep1-skill'); + expect(skillNames, isNot(contains('dep2-skill')), + reason: 'then manifest should not contain dep2-skill'); + }); + + test('when running `skills get --packages dep1` (non-interactive)', + () async { + final getCommand = GetCommand( + dialogSupport: null, + gitRunner: GitRunner(isAvailableOverride: () async => false), + ); + final runner = CommandRunner('skills', 'Test') + ..addCommand(getCommand); + + await runner.run([ + 'get', + '--directory', + projectPath, + '--ide', + Ide.generic.cliName, + '--packages', + 'dep1' + ]); + + final dep1SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep1-skill'), + ); + final dep2SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep2-skill'), + ); + + expect(await dep1SkillDir.exists(), isTrue); + expect(await dep2SkillDir.exists(), isFalse); + }); + + test('when running `skills get --packages all` (non-interactive)', + () async { + final getCommand = GetCommand( + dialogSupport: null, + gitRunner: GitRunner(isAvailableOverride: () async => false), + ); + final runner = CommandRunner('skills', 'Test') + ..addCommand(getCommand); + + await runner.run([ + 'get', + '--directory', + projectPath, + '--ide', + Ide.generic.cliName, + '--packages', + 'all' + ]); + + final dep1SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep1-skill'), + ); + final dep2SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep2-skill'), + ); + + expect(await dep1SkillDir.exists(), isTrue); + expect(await dep2SkillDir.exists(), isTrue); + }); + + test('when running `skills get` without --packages and NO dialog support', + () async { + final getCommand = GetCommand( + dialogSupport: null, + gitRunner: GitRunner(isAvailableOverride: () async => false), + ); + final runner = CommandRunner('skills', 'Test') + ..addCommand(getCommand); + + await runner.run([ + 'get', + '--directory', + projectPath, + '--ide', + Ide.generic.cliName, + ]); + + final dep1SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep1-skill'), + ); + final dep2SkillDir = Directory( + p.join(skillsAdapter.skillsDirectory, 'dep2-skill'), + ); + + expect(await dep1SkillDir.exists(), isFalse); + expect(await dep2SkillDir.exists(), isFalse); + }); + }); +} diff --git a/test/fake_dialog_support.dart b/test/fake_dialog_support.dart index 5f88141..34610de 100644 --- a/test/fake_dialog_support.dart +++ b/test/fake_dialog_support.dart @@ -12,6 +12,7 @@ class FakeDialogSupport implements DialogSupport { List? lastSingleSelectOptions; List? lastMultiSelectOptions; + Set? lastInitialSelected; @override Future showSingleSelectDialog(List options, @@ -21,9 +22,13 @@ class FakeDialogSupport implements DialogSupport { } @override - Future?> showMultiSelectDialog(List options, - {String? title}) async { + Future?> showMultiSelectDialog( + List options, { + String? title, + Set initialSelected = const {}, + }) async { lastMultiSelectOptions = options; + lastInitialSelected = initialSelected; return multiSelectResult; } } diff --git a/test/integration/multi_ide_lifecycle_test.dart b/test/integration/multi_ide_lifecycle_test.dart index 11a1ed9..1610faa 100644 --- a/test/integration/multi_ide_lifecycle_test.dart +++ b/test/integration/multi_ide_lifecycle_test.dart @@ -203,7 +203,7 @@ Instructions for debugging. ide: ide, rootPath: rootPath, manifest: manifest, - packageName: 'pkg_a', + packageNames: {'pkg_a'}, ); manifest = result.manifest; } From 9b54eb16f7dd7c1578c6efcb949634941952c2ed Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 8 May 2026 21:10:50 +0000 Subject: [PATCH 02/13] Fix tests to use rest arguments and add remove command tests --- lib/src/commands/get_skills.dart | 3 +- lib/src/commands/prune_command.dart | 2 +- lib/src/core/skill_installer.dart | 2 +- .../get_command_select_packages_test.dart | 10 +- test/commands/remove_command_test.dart | 355 ++++++------------ 5 files changed, 119 insertions(+), 253 deletions(-) diff --git a/lib/src/commands/get_skills.dart b/lib/src/commands/get_skills.dart index fab7569..33646dd 100644 --- a/lib/src/commands/get_skills.dart +++ b/lib/src/commands/get_skills.dart @@ -88,8 +88,7 @@ Future getSkills({ for (final pkg in packagesWithSkills) { logger.info(' $pkg'); } - logger.info( - 'Rerun with trailing arguments for each package you want ' + logger.info('Rerun with trailing arguments for each package you want ' 'to install skills for, or `all` to install all skills.'); return false; } diff --git a/lib/src/commands/prune_command.dart b/lib/src/commands/prune_command.dart index 2319ae4..79106e5 100644 --- a/lib/src/commands/prune_command.dart +++ b/lib/src/commands/prune_command.dart @@ -67,7 +67,7 @@ class PruneCommand extends SkillsCommand { final pkgs = manifest.packagesForIde(ide.cliName); final pkgsToPrune = pkgs.keys.where((name) => !referencedNames.contains(name)).toSet(); - prunedPackages.addAll(pkgsToPrune); + prunedPackages.addAll(pkgsToPrune); final result = await installer.removeSkillsForIde( ide: ide, diff --git a/lib/src/core/skill_installer.dart b/lib/src/core/skill_installer.dart index 4ba87da..a4d57e8 100644 --- a/lib/src/core/skill_installer.dart +++ b/lib/src/core/skill_installer.dart @@ -126,7 +126,7 @@ class SkillInstaller { } /// Removes skills for [ide] from [manifest]. - /// + /// /// If [packageNames] is set, only those packages are removed; otherwise all. /// If [packageNames] contains `all`, then all packages are also removed. Future removeSkillsForIde({ diff --git a/test/commands/get_command_select_packages_test.dart b/test/commands/get_command_select_packages_test.dart index 863c976..2f587a7 100644 --- a/test/commands/get_command_select_packages_test.dart +++ b/test/commands/get_command_select_packages_test.dart @@ -118,8 +118,7 @@ environment: reason: 'then manifest should not contain dep2-skill'); }); - test('when running `skills get --packages dep1` (non-interactive)', - () async { + test('when running `skills get dep1` (non-interactive)', () async { final getCommand = GetCommand( dialogSupport: null, gitRunner: GitRunner(isAvailableOverride: () async => false), @@ -133,7 +132,6 @@ environment: projectPath, '--ide', Ide.generic.cliName, - '--packages', 'dep1' ]); @@ -148,8 +146,7 @@ environment: expect(await dep2SkillDir.exists(), isFalse); }); - test('when running `skills get --packages all` (non-interactive)', - () async { + test('when running `skills get all` (non-interactive)', () async { final getCommand = GetCommand( dialogSupport: null, gitRunner: GitRunner(isAvailableOverride: () async => false), @@ -163,7 +160,6 @@ environment: projectPath, '--ide', Ide.generic.cliName, - '--packages', 'all' ]); @@ -178,7 +174,7 @@ environment: expect(await dep2SkillDir.exists(), isTrue); }); - test('when running `skills get` without --packages and NO dialog support', + test('when running `skills get` without package arguments and NO dialog support', () async { final getCommand = GetCommand( dialogSupport: null, diff --git a/test/commands/remove_command_test.dart b/test/commands/remove_command_test.dart index cf66758..4514ca0 100644 --- a/test/commands/remove_command_test.dart +++ b/test/commands/remove_command_test.dart @@ -1,276 +1,147 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:args/command_runner.dart'; import 'package:path/path.dart' as p; -import 'package:skills/src/ide/adapters/claude_adapter.dart'; -import 'package:skills/src/ide/adapters/cursor_adapter.dart'; +import 'package:skills/src/commands/remove_command.dart'; import 'package:skills/src/models/skill_manifest.dart'; +import '../fake_dialog_support.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { - group('Given a project with managed skills installed', () { + group('Given a project with installed skills for dep1 and dep2', () { late String projectPath; - late SkillManifest manifest; + late FakeDialogSupport fakeDialogSupport; setUp(() async { - await d.dir('project', [ + final projectRootDir = d.dir('project', [ + d.file('pubspec.yaml', ''' +name: test_app +environment: + sdk: ^3.0.0 +'''), + d.dir('.dart_tool', [ + d.file( + 'package_config.json', + jsonEncode({ + 'configVersion': 2, + 'packages': [ + {'name': 'test_app', 'rootUri': '../', 'packageUri': 'lib/'}, + {'name': 'dep1', 'rootUri': '../../dep1', 'packageUri': 'lib/'}, + {'name': 'dep2', 'rootUri': '../../dep2', 'packageUri': 'lib/'}, + ], + }), + ), + ]), d.dir('.cursor', [ d.dir('skills', [ - d.dir('pkg_a-skill-1', [ - d.file( - 'SKILL.md', - '---\nname: pkg_a-skill-1\ndescription: s1\n---\nBody 1', - ), - ]), - d.dir('pkg_a-skill-2', [ - d.file( - 'SKILL.md', - '---\nname: pkg_a-skill-2\ndescription: s2\n---\nBody 2', - ), - ]), - d.dir('pkg_b-skill-3', [ - d.file( - 'SKILL.md', - '---\nname: pkg_b-skill-3\ndescription: s3\n---\nBody 3', - ), - ]), + d.dir('dep1-skill', [d.file('SKILL.md', 'content')]), + d.dir('dep2-skill', [d.file('SKILL.md', 'content')]), ]), ]), - ]).create(); - - projectPath = d.path('project'); - - manifest = SkillManifest( - installations: { - 'cursor': { - 'pkg_a': PackageSkillsEntry( - skills: [ - InstalledSkillEntry( - name: 'pkg_a-skill-1', - installedAt: DateTime.utc(2026), - ), - InstalledSkillEntry( - name: 'pkg_a-skill-2', - installedAt: DateTime.utc(2026), - ), - ], - ), - 'pkg_b': PackageSkillsEntry( - skills: [ - InstalledSkillEntry( - name: 'pkg_b-skill-3', - installedAt: DateTime.utc(2026), - ), - ], - ), - }, - }, - ); - - await manifest.save(File(SkillManifest.pathIn(projectPath))); - }); - - test( - 'when removing a specific package then only its skills are removed', - () async { - final adapter = CursorAdapter(projectPath); - - final pkgEntry = manifest.packagesForIde('cursor')['pkg_a']!; - for (final skill in pkgEntry.skills) { - await adapter.removeSkill(skill.name); - } - final updated = manifest.withoutPackage('cursor', 'pkg_a'); - - expect( - await Directory('$projectPath/.cursor/skills/pkg_a-skill-1').exists(), - isFalse, - ); - expect( - await Directory('$projectPath/.cursor/skills/pkg_a-skill-2').exists(), - isFalse, - ); - expect( - await Directory('$projectPath/.cursor/skills/pkg_b-skill-3').exists(), - isTrue, - ); - expect(updated.packagesForIde('cursor'), hasLength(1)); - expect(updated.packagesForIde('cursor'), contains('pkg_b')); - }, - ); - - test('when removing all then all managed skills are removed', () async { - final adapter = CursorAdapter(projectPath); - - for (final entry in manifest.packagesForIde('cursor').values) { - for (final skill in entry.skills) { - await adapter.removeSkill(skill.name); - } - } + d.dir('.dart_skills', [ + d.file( + 'skills_config.json', + jsonEncode({ + 'version': 1, + 'installations': { + 'cursor': { + 'dep1': { + 'skills': [ + { + 'name': 'dep1-skill', + 'installedAt': '2026-05-08T18:00:00Z' + } + ] + }, + 'dep2': { + 'skills': [ + { + 'name': 'dep2-skill', + 'installedAt': '2026-05-08T18:00:00Z' + } + ] + } + } + } + })), + ]), + ]); + await projectRootDir.create(); - expect( - await Directory('$projectPath/.cursor/skills/pkg_a-skill-1').exists(), - isFalse, - ); - expect( - await Directory('$projectPath/.cursor/skills/pkg_b-skill-3').exists(), - isFalse, - ); + projectPath = projectRootDir.io.path; + fakeDialogSupport = FakeDialogSupport(); }); - test( - 'when removing all then .dart_skills directory is cleaned up', - () async { - final updated = manifest - .withoutPackage('cursor', 'pkg_a') - .withoutPackage('cursor', 'pkg_b'); - expect(updated.isEmpty, isTrue); - - final dartSkillsDir = Directory( - p.join(projectPath, SkillManifest.dirName), - ); - expect(await dartSkillsDir.exists(), isTrue); - - await SkillManifest.cleanupDir(projectPath); - - expect(await dartSkillsDir.exists(), isFalse); - }, - ); - }); - - group('Given a project with no managed skills', () { - test('when removing then manifest remains empty', () async { - await d.dir('empty_project', [ - d.dir('.cursor', [d.dir('skills')]), - ]).create(); - - final manifestFile = File(SkillManifest.pathIn(d.path('empty_project'))); - final manifest = await SkillManifest.load(manifestFile); - - expect(manifest, isNull); + test('when running `skills remove dep1` then removes only dep1 skills', + () async { + final removeCommand = RemoveCommand(dialogSupport: fakeDialogSupport); + final runner = CommandRunner('skills', 'Test') + ..addCommand(removeCommand); + + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'cursor', 'dep1']); + + final dep1SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); + final dep2SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); + + expect(await dep1SkillDir.exists(), isFalse, + reason: 'dep1 skill should be removed'); + expect(await dep2SkillDir.exists(), isTrue, + reason: 'dep2 skill should still exist'); + + final manifestFile = File(SkillManifest.pathIn(projectPath)); + final manifest = await SkillManifest.loadOrEmpty(manifestFile); + final skillNames = + manifest.allSkillsForIde('cursor').map((e) => e.name).toSet(); + expect(skillNames, isNot(contains('dep1-skill'))); + expect(skillNames, contains('dep2-skill')); }); - }); - - group('Given a project with multi-IDE installations (Cursor + Claude)', () { - late String projectPath; - late SkillManifest manifest; - - setUp(() async { - await d.dir('multi_project', [ - d.dir('.cursor', [ - d.dir('skills', [ - d.dir('pkg-skill-a', [ - d.file( - 'SKILL.md', - '---\nname: pkg-skill-a\ndescription: a\n---\nBody A', - ), - ]), - ]), - ]), - d.dir('.claude', [ - d.dir('skills', [ - d.dir('pkg-skill-a', [ - d.file( - 'SKILL.md', - '---\nname: pkg-skill-a\ndescription: a\n---\nBody A', - ), - ]), - ]), - ]), - ]).create(); - projectPath = d.path('multi_project'); + test('when running `skills remove all` then removes all skills', () async { + final removeCommand = RemoveCommand(dialogSupport: fakeDialogSupport); + final runner = CommandRunner('skills', 'Test') + ..addCommand(removeCommand); - manifest = SkillManifest( - installations: { - 'cursor': { - 'pkg': PackageSkillsEntry( - skills: [ - InstalledSkillEntry( - name: 'pkg-skill-a', - installedAt: DateTime.utc(2026), - ), - ], - ), - }, - 'claude': { - 'pkg': PackageSkillsEntry( - skills: [ - InstalledSkillEntry( - name: 'pkg-skill-a', - installedAt: DateTime.utc(2026), - ), - ], - ), - }, - }, - ); + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'cursor', 'all']); - await manifest.save(File(SkillManifest.pathIn(projectPath))); - }); + final dep1SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); + final dep2SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); - test( - 'when removing one IDE then files for that IDE are deleted and ' - 'other IDE files remain', () async { - final cursorAdapter = CursorAdapter(projectPath); + expect(await dep1SkillDir.exists(), isFalse); + expect(await dep2SkillDir.exists(), isFalse); - for (final skill in manifest.packagesForIde('cursor')['pkg']!.skills) { - await cursorAdapter.removeSkill(skill.name); - } - final updated = manifest.withoutIde('cursor'); - - expect( - Directory('$projectPath/.cursor/skills/pkg-skill-a').existsSync(), - isFalse, - ); - expect( - Directory('$projectPath/.claude/skills/pkg-skill-a').existsSync(), - isTrue, - ); - expect(updated.allIdes, equals(['claude'])); - expect(updated.packagesForIde('claude')['pkg']!.skills, hasLength(1)); + final manifestDir = Directory(p.join(projectPath, '.dart_skills')); + expect(await manifestDir.exists(), isFalse, + reason: 'Manifest dir should be deleted when empty'); }); test( - 'when removing all IDEs then both Cursor and Claude skill ' - 'directories are deleted', () async { - final cursorAdapter = CursorAdapter(projectPath); - final claudeAdapter = ClaudeAdapter(projectPath); - - for (final skill in manifest.packagesForIde('cursor')['pkg']!.skills) { - await cursorAdapter.removeSkill(skill.name); - } - var updated = manifest.withoutIde('cursor'); + 'when running `skills remove` without arguments then removes all skills for that IDE', + () async { + final removeCommand = RemoveCommand(dialogSupport: fakeDialogSupport); + final runner = CommandRunner('skills', 'Test') + ..addCommand(removeCommand); - for (final skill in updated.packagesForIde('claude')['pkg']!.skills) { - await claudeAdapter.removeSkill(skill.name); - } - updated = updated.withoutIde('claude'); + await runner + .run(['remove', '--directory', projectPath, '--ide', 'cursor']); - expect( - Directory('$projectPath/.cursor/skills/pkg-skill-a').existsSync(), - isFalse, - ); - expect( - Directory('$projectPath/.claude/skills/pkg-skill-a').existsSync(), - isFalse, - ); - expect(updated.isEmpty, isTrue); - }); - - test( - 'when Claude skill directory is manually deleted then remove still ' - 'cleans manifest without error', () async { - Directory( - '$projectPath/.claude/skills/pkg-skill-a', - ).deleteSync(recursive: true); + final dep1SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); + final dep2SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); - final claudeAdapter = ClaudeAdapter(projectPath); - for (final skill in manifest.packagesForIde('claude')['pkg']!.skills) { - await claudeAdapter.removeSkill(skill.name); - } - final updated = manifest.withoutIde('claude'); + expect(await dep1SkillDir.exists(), isFalse); + expect(await dep2SkillDir.exists(), isFalse); - expect(updated.allIdes, equals(['cursor'])); + final manifestDir = Directory(p.join(projectPath, '.dart_skills')); + expect(await manifestDir.exists(), isFalse); }); }); } From b8e3f8d1fcebac970da68b0e8e14b9849070ab27 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 8 May 2026 21:26:27 +0000 Subject: [PATCH 03/13] Require selection for get/remove commands if packages not specified --- lib/src/commands/remove_command.dart | 48 +++++++++++++++++-- .../get_command_select_packages_test.dart | 3 +- test/commands/remove_command_test.dart | 28 ++++++++++- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/lib/src/commands/remove_command.dart b/lib/src/commands/remove_command.dart index 6db48d0..05b80bf 100644 --- a/lib/src/commands/remove_command.dart +++ b/lib/src/commands/remove_command.dart @@ -35,7 +35,7 @@ class RemoveCommand extends SkillsCommand { var manifest = loaded; - final packageName = packageNamesArg; + var packagesToRemove = packageNamesArg?.toSet(); // Determine which IDEs to remove from: --ide narrows to one, // otherwise all IDEs in the manifest. @@ -50,6 +50,46 @@ class RemoveCommand extends SkillsCommand { .toList(); } + if (packagesToRemove == null) { + final packagesWithSkills = {}; + for (final ide in targetIdes) { + packagesWithSkills.addAll(manifest.packagesForIde(ide.cliName).keys); + } + final packagesList = packagesWithSkills.toList()..sort(); + + if (packagesList.isEmpty) { + logger.info('No skills found to remove.'); + return; + } + + if (_dialogSupport != null) { + final selectedIndices = await _dialogSupport.showMultiSelectDialog( + packagesList, + title: 'Select packages to remove skills for:', + ); + if (selectedIndices != null) { + packagesToRemove = + selectedIndices.map((i) => packagesList[i]).toSet(); + } else { + logger.info('Removal aborted.'); + return; + } + } else { + logger.info('Packages with installed skills:'); + for (final pkg in packagesList) { + logger.info(' $pkg'); + } + logger.info('Rerun with trailing arguments for each package you want ' + 'to remove skills for, or `all` to remove all skills.'); + return; + } + } + + if (packagesToRemove.isEmpty) { + logger.info('No packages selected for removal.'); + return; + } + final installer = SkillInstaller(_dialogSupport); var totalRemoved = 0; @@ -58,7 +98,7 @@ class RemoveCommand extends SkillsCommand { ide: ide, rootPath: rootPath, manifest: manifest, - packageNames: packageNamesArg?.toSet(), + packageNames: packagesToRemove, ); manifest = result.manifest; totalRemoved += result.removedCount; @@ -73,8 +113,8 @@ class RemoveCommand extends SkillsCommand { await manifest.save(manifestFile(rootPath)); } - if (packageName != null) { - logger.info('Removed skills from $packageName.'); + if (packagesToRemove.isNotEmpty) { + logger.info('Removed skills from ${packagesToRemove.join(', ')}.'); } else { logger.info('Removed $totalRemoved managed skill(s).'); } diff --git a/test/commands/get_command_select_packages_test.dart b/test/commands/get_command_select_packages_test.dart index 2f587a7..507cfe1 100644 --- a/test/commands/get_command_select_packages_test.dart +++ b/test/commands/get_command_select_packages_test.dart @@ -174,7 +174,8 @@ environment: expect(await dep2SkillDir.exists(), isTrue); }); - test('when running `skills get` without package arguments and NO dialog support', + test( + 'when running `skills get` without package arguments and NO dialog support', () async { final getCommand = GetCommand( dialogSupport: null, diff --git a/test/commands/remove_command_test.dart b/test/commands/remove_command_test.dart index 4514ca0..7ae550f 100644 --- a/test/commands/remove_command_test.dart +++ b/test/commands/remove_command_test.dart @@ -123,8 +123,9 @@ environment: }); test( - 'when running `skills remove` without arguments then removes all skills for that IDE', + 'when running `skills remove` without arguments and selecting all then removes all skills for that IDE', () async { + fakeDialogSupport.multiSelectResult = {0, 1}; final removeCommand = RemoveCommand(dialogSupport: fakeDialogSupport); final runner = CommandRunner('skills', 'Test') ..addCommand(removeCommand); @@ -143,5 +144,30 @@ environment: final manifestDir = Directory(p.join(projectPath, '.dart_skills')); expect(await manifestDir.exists(), isFalse); }); + + test( + 'when running `skills remove` without arguments and NO dialog support then does nothing and prints packages', + () async { + final removeCommand = RemoveCommand(dialogSupport: null); + final runner = CommandRunner('skills', 'Test') + ..addCommand(removeCommand); + + await runner + .run(['remove', '--directory', projectPath, '--ide', 'cursor']); + + final dep1SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); + final dep2SkillDir = + Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); + + expect(await dep1SkillDir.exists(), isTrue, + reason: 'skills should still exist'); + expect(await dep2SkillDir.exists(), isTrue, + reason: 'skills should still exist'); + + final manifestDir = Directory(p.join(projectPath, '.dart_skills')); + expect(await manifestDir.exists(), isTrue, + reason: 'manifest should still exist'); + }); }); } From 4ac6a7baa4339384e181c6cea8e17fb84536c51e Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Mon, 11 May 2026 14:45:02 +0000 Subject: [PATCH 04/13] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 874cd09..89734ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## 0.4.0-dev -- feat: Added a dialog to select which packages to install skills from during `skills get`. +- feat: Added a dialog to select which packages to install skills from during + `skills get` and `skills remove`. +- feat: Support passing multiple package names as trailing arguments. - refactor: Migrate from `.agent/skills` to `.agents/skills` for the generic IDE adapter. When a `.agent/` dir is detected you will be prompted for what action to take. From 1f1f0b3a0afe622474a6c8131290d5d483c80e48 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Mon, 11 May 2026 21:32:43 +0000 Subject: [PATCH 05/13] use test descriptor validate functions --- .jetskicli/project.json | 1 + .../get_command_select_packages_test.dart | 28 ++--- test/commands/remove_command_test.dart | 103 ++++++++---------- 3 files changed, 64 insertions(+), 68 deletions(-) create mode 120000 .jetskicli/project.json diff --git a/.jetskicli/project.json b/.jetskicli/project.json new file mode 120000 index 0000000..1683aa6 --- /dev/null +++ b/.jetskicli/project.json @@ -0,0 +1 @@ +/usr/local/google/home/jakemac/.gemini/config/projects/993a0563-83ff-4bbb-8123-4ba4961c952e.json \ No newline at end of file diff --git a/test/commands/get_command_select_packages_test.dart b/test/commands/get_command_select_packages_test.dart index 507cfe1..4f160cd 100644 --- a/test/commands/get_command_select_packages_test.dart +++ b/test/commands/get_command_select_packages_test.dart @@ -76,7 +76,9 @@ environment: skillsAdapter = GenericAdapter(projectPath, fakeDialogSupport); }); - test('when running `skills get` and the user only selects dep1', () async { + test( + 'when running `skills get` and the user only selects dep1 then only ' + 'dep1 should be installed', () async { fakeDialogSupport.multiSelectResult = {0}; final getCommand = GetCommand( dialogSupport: fakeDialogSupport, @@ -99,10 +101,8 @@ environment: p.join(skillsAdapter.skillsDirectory, 'dep2-skill'), ); - expect(await dep1SkillDir.exists(), isTrue, - reason: 'then dep1 skill should be installed'); - expect(await dep2SkillDir.exists(), isFalse, - reason: 'then dep2 skill should NOT be installed'); + expect(await dep1SkillDir.exists(), isTrue); + expect(await dep2SkillDir.exists(), isFalse); final manifestFile = File(SkillManifest.pathIn(projectPath)); expect(await manifestFile.exists(), isTrue); @@ -112,13 +112,13 @@ environment: .allSkillsForIde(Ide.generic.cliName) .map((e) => e.name) .toSet(); - expect(skillNames, contains('dep1-skill'), - reason: 'then manifest should contain dep1-skill'); - expect(skillNames, isNot(contains('dep2-skill')), - reason: 'then manifest should not contain dep2-skill'); + expect(skillNames, contains('dep1-skill')); + expect(skillNames, isNot(contains('dep2-skill'))); }); - test('when running `skills get dep1` (non-interactive)', () async { + test( + 'when running `skills get dep1` (non-interactive) then only dep1 ' + 'should be installed', () async { final getCommand = GetCommand( dialogSupport: null, gitRunner: GitRunner(isAvailableOverride: () async => false), @@ -146,7 +146,9 @@ environment: expect(await dep2SkillDir.exists(), isFalse); }); - test('when running `skills get all` (non-interactive)', () async { + test( + 'when running `skills get all` (non-interactive) then all skills ' + 'should be installed', () async { final getCommand = GetCommand( dialogSupport: null, gitRunner: GitRunner(isAvailableOverride: () async => false), @@ -175,8 +177,8 @@ environment: }); test( - 'when running `skills get` without package arguments and NO dialog support', - () async { + 'when running `skills get` without package arguments and NO dialog ' + 'support then no skills should be installed', () async { final getCommand = GetCommand( dialogSupport: null, gitRunner: GitRunner(isAvailableOverride: () async => false), diff --git a/test/commands/remove_command_test.dart b/test/commands/remove_command_test.dart index 7ae550f..76ee32a 100644 --- a/test/commands/remove_command_test.dart +++ b/test/commands/remove_command_test.dart @@ -1,10 +1,7 @@ import 'dart:convert'; -import 'dart:io'; import 'package:args/command_runner.dart'; -import 'package:path/path.dart' as p; import 'package:skills/src/commands/remove_command.dart'; -import 'package:skills/src/models/skill_manifest.dart'; import '../fake_dialog_support.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -83,22 +80,23 @@ environment: await runner.run( ['remove', '--directory', projectPath, '--ide', 'cursor', 'dep1']); - final dep1SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); - final dep2SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); - - expect(await dep1SkillDir.exists(), isFalse, - reason: 'dep1 skill should be removed'); - expect(await dep2SkillDir.exists(), isTrue, - reason: 'dep2 skill should still exist'); - - final manifestFile = File(SkillManifest.pathIn(projectPath)); - final manifest = await SkillManifest.loadOrEmpty(manifestFile); - final skillNames = - manifest.allSkillsForIde('cursor').map((e) => e.name).toSet(); - expect(skillNames, isNot(contains('dep1-skill'))); - expect(skillNames, contains('dep2-skill')); + await d.dir('project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + d.dir('.dart_skills', [ + d.file( + 'skills_config.json', + allOf( + isNot(contains('dep1-skill')), + contains('dep2-skill'), + ), + ), + ]), + ]).validate(); }); test('when running `skills remove all` then removes all skills', () async { @@ -109,17 +107,15 @@ environment: await runner.run( ['remove', '--directory', projectPath, '--ide', 'cursor', 'all']); - final dep1SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); - final dep2SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); - - expect(await dep1SkillDir.exists(), isFalse); - expect(await dep2SkillDir.exists(), isFalse); - - final manifestDir = Directory(p.join(projectPath, '.dart_skills')); - expect(await manifestDir.exists(), isFalse, - reason: 'Manifest dir should be deleted when empty'); + await d.dir('project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.nothing('dep2-skill'), + ]), + ]), + d.nothing('.dart_skills'), + ]).validate(); }); test( @@ -133,21 +129,20 @@ environment: await runner .run(['remove', '--directory', projectPath, '--ide', 'cursor']); - final dep1SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); - final dep2SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); - - expect(await dep1SkillDir.exists(), isFalse); - expect(await dep2SkillDir.exists(), isFalse); - - final manifestDir = Directory(p.join(projectPath, '.dart_skills')); - expect(await manifestDir.exists(), isFalse); + await d.dir('project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.nothing('dep2-skill'), + ]), + ]), + d.nothing('.dart_skills'), + ]).validate(); }); test( - 'when running `skills remove` without arguments and NO dialog support then does nothing and prints packages', - () async { + 'when running `skills remove` without arguments and NO dialog support ' + 'then does nothing and prints packages', () async { final removeCommand = RemoveCommand(dialogSupport: null); final runner = CommandRunner('skills', 'Test') ..addCommand(removeCommand); @@ -155,19 +150,17 @@ environment: await runner .run(['remove', '--directory', projectPath, '--ide', 'cursor']); - final dep1SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep1-skill')); - final dep2SkillDir = - Directory(p.join(projectPath, '.cursor', 'skills', 'dep2-skill')); - - expect(await dep1SkillDir.exists(), isTrue, - reason: 'skills should still exist'); - expect(await dep2SkillDir.exists(), isTrue, - reason: 'skills should still exist'); - - final manifestDir = Directory(p.join(projectPath, '.dart_skills')); - expect(await manifestDir.exists(), isTrue, - reason: 'manifest should still exist'); + await d.dir('project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.dir('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + d.dir('.dart_skills', [ + d.file('skills_config.json', contains('dep1-skill')), + ]), + ]).validate(); }); }); } From 303424f943a5ee8c97ffa1dc9bf444f52a3a0ba0 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Mon, 11 May 2026 21:36:03 +0000 Subject: [PATCH 06/13] make note of breaking change to getSkills in the changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89734ae..feb6456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - feat: Added a dialog to select which packages to install skills from during `skills get` and `skills remove`. - feat: Support passing multiple package names as trailing arguments. + - **Breaking Change**: `getSkills` now takes a set of package names to install + instead of just a single package name. - refactor: Migrate from `.agent/skills` to `.agents/skills` for the generic IDE adapter. When a `.agent/` dir is detected you will be prompted for what action to take. From 5ffeba2892a4baedcd36fdd82b011ede5c725fbf Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Mon, 11 May 2026 21:54:57 +0000 Subject: [PATCH 07/13] set up log listeners in all tests to print on failure --- test/commands/get_command_registry_test.dart | 5 +++++ test/commands/get_command_select_packages_test.dart | 5 +++++ test/commands/get_command_test.dart | 5 +++++ test/commands/list_command_test.dart | 5 +++++ test/commands/prune_command_test.dart | 5 +++++ test/commands/remove_command_test.dart | 5 +++++ test/core/git_runner_test.dart | 5 +++++ test/core/package_resolver_test.dart | 5 +++++ test/core/registry_repos_test.dart | 5 +++++ test/core/registry_scanner_test.dart | 5 +++++ test/core/registry_sync_test.dart | 4 ++++ test/core/skill_installer_test.dart | 5 +++++ test/core/skill_merger_test.dart | 5 +++++ test/core/skill_scanner_test.dart | 4 ++++ test/core/workspace_resolver_test.dart | 5 +++++ test/ide/claude_adapter_test.dart | 5 +++++ test/ide/cline_adapter_test.dart | 5 +++++ test/ide/copilot_adapter_test.dart | 5 +++++ test/ide/cursor_adapter_test.dart | 5 +++++ test/ide/generic_adapter_test.dart | 4 ++++ test/ide/ide_detection_test.dart | 5 +++++ test/ide/opencode_adapter_test.dart | 5 +++++ test/integration/multi_ide_lifecycle_test.dart | 5 +++++ test/models/skill_manifest_test.dart | 5 +++++ test/models/skill_metadata_test.dart | 5 +++++ 25 files changed, 122 insertions(+) diff --git a/test/commands/get_command_registry_test.dart b/test/commands/get_command_registry_test.dart index afb179a..7be59e9 100644 --- a/test/commands/get_command_registry_test.dart +++ b/test/commands/get_command_registry_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/commands/get_command.dart'; import 'package:skills/src/core/git_runner.dart'; @@ -11,6 +12,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('GetCommand with registry', () { test( 'when git is unavailable then only Dart skills are installed and warning is printed', diff --git a/test/commands/get_command_select_packages_test.dart b/test/commands/get_command_select_packages_test.dart index 4f160cd..5fb9fab 100644 --- a/test/commands/get_command_select_packages_test.dart +++ b/test/commands/get_command_select_packages_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/commands/get_command.dart'; import 'package:skills/src/core/git_runner.dart'; @@ -19,6 +20,10 @@ void main() { late FakeDialogSupport fakeDialogSupport; late AgentSkillsAdapter skillsAdapter; + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + setUp(() async { final dep1Dir = d.dir('dep1', [ d.dir('lib', [d.file('dep1.dart', '')]), diff --git a/test/commands/get_command_test.dart b/test/commands/get_command_test.dart index e3593f5..a55293c 100644 --- a/test/commands/get_command_test.dart +++ b/test/commands/get_command_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/cursor_adapter.dart'; import 'package:skills/src/models/skill_manifest.dart'; @@ -7,6 +8,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a project with dependencies containing pre-prefixed skills', () { late String projectPath; diff --git a/test/commands/list_command_test.dart b/test/commands/list_command_test.dart index c30fdd4..58604e3 100644 --- a/test/commands/list_command_test.dart +++ b/test/commands/list_command_test.dart @@ -1,10 +1,15 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/models/skill_manifest.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a project with installed skills in multiple IDEs', () { late SkillManifest manifest; diff --git a/test/commands/prune_command_test.dart b/test/commands/prune_command_test.dart index 4abe625..fb0fd5a 100644 --- a/test/commands/prune_command_test.dart +++ b/test/commands/prune_command_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/commands/prune_command.dart'; import 'package:skills/src/models/skill_manifest.dart'; @@ -10,6 +11,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Prune command', () { test( 'removes only unreferenced packages when manifest has pkg_a and pkg_b but deps only pkg_a', diff --git a/test/commands/remove_command_test.dart b/test/commands/remove_command_test.dart index 76ee32a..6441e72 100644 --- a/test/commands/remove_command_test.dart +++ b/test/commands/remove_command_test.dart @@ -1,12 +1,17 @@ import 'dart:convert'; import 'package:args/command_runner.dart'; +import 'package:logging/logging.dart'; import 'package:skills/src/commands/remove_command.dart'; import '../fake_dialog_support.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a project with installed skills for dep1 and dep2', () { late String projectPath; late FakeDialogSupport fakeDialogSupport; diff --git a/test/core/git_runner_test.dart b/test/core/git_runner_test.dart index 19fc1b3..5c1619c 100644 --- a/test/core/git_runner_test.dart +++ b/test/core/git_runner_test.dart @@ -1,7 +1,12 @@ +import 'package:logging/logging.dart'; import 'package:skills/src/core/git_runner.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('GitRunner', () { test('when override returns false then isAvailable is false', () async { const runner = GitRunner(isAvailableOverride: _returnFalse); diff --git a/test/core/package_resolver_test.dart b/test/core/package_resolver_test.dart index 52f9f40..fca9bfe 100644 --- a/test/core/package_resolver_test.dart +++ b/test/core/package_resolver_test.dart @@ -1,10 +1,15 @@ import 'dart:convert'; +import 'package:logging/logging.dart'; import 'package:skills/src/core/package_resolver.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a project with a valid package_config.json', () { late String projectPath; diff --git a/test/core/registry_repos_test.dart b/test/core/registry_repos_test.dart index 0017a77..1a35cfc 100644 --- a/test/core/registry_repos_test.dart +++ b/test/core/registry_repos_test.dart @@ -1,9 +1,14 @@ +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/config.dart'; import 'package:skills/src/core/registry_repos.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('RegistryRepo', () { test('pathSegment joins owner and name', () { const repo = RegistryRepo( diff --git a/test/core/registry_scanner_test.dart b/test/core/registry_scanner_test.dart index 26a1b43..5529e50 100644 --- a/test/core/registry_scanner_test.dart +++ b/test/core/registry_scanner_test.dart @@ -1,3 +1,4 @@ +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/registry_repos.dart'; import 'package:skills/src/core/registry_scanner.dart'; @@ -5,6 +6,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('RegistryScanner', () { test('when repos directory does not exist then returns empty', () async { await d.dir('project', []).create(); diff --git a/test/core/registry_sync_test.dart b/test/core/registry_sync_test.dart index e234365..823c5f3 100644 --- a/test/core/registry_sync_test.dart +++ b/test/core/registry_sync_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/registry_repos.dart'; import 'package:skills/src/core/registry_sync.dart'; @@ -7,6 +8,9 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); group('RegistrySync', () { test( 'when repos dir does not exist then creates it and clones local repo', diff --git a/test/core/skill_installer_test.dart b/test/core/skill_installer_test.dart index 6034a87..01c591b 100644 --- a/test/core/skill_installer_test.dart +++ b/test/core/skill_installer_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/skill_installer.dart'; import 'package:skills/src/core/skill_scanner.dart'; @@ -9,6 +10,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given an existing project that needs migration', () { late String rootPath; late List scannedSkills; diff --git a/test/core/skill_merger_test.dart b/test/core/skill_merger_test.dart index e809151..3dc0c23 100644 --- a/test/core/skill_merger_test.dart +++ b/test/core/skill_merger_test.dart @@ -1,8 +1,13 @@ +import 'package:logging/logging.dart'; import 'package:skills/src/core/skill_merger.dart'; import 'package:skills/src/core/skill_scanner.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('mergeSkills', () { test( 'when package has both dart and registry skills then only dart included', diff --git a/test/core/skill_scanner_test.dart b/test/core/skill_scanner_test.dart index 1818cef..5b0842d 100644 --- a/test/core/skill_scanner_test.dart +++ b/test/core/skill_scanner_test.dart @@ -8,6 +8,10 @@ import 'package:test_descriptor/test_descriptor.dart' as d; void main() { late Logger logger; + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a package with correctly prefixed skills', () { late ResolvedPackage package; diff --git a/test/core/workspace_resolver_test.dart b/test/core/workspace_resolver_test.dart index be917e3..59b09fe 100644 --- a/test/core/workspace_resolver_test.dart +++ b/test/core/workspace_resolver_test.dart @@ -1,9 +1,14 @@ +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/workspace_resolver.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a single-package project', () { test( 'when resolving then returns one package pointing at project root', diff --git a/test/ide/claude_adapter_test.dart b/test/ide/claude_adapter_test.dart index 1fafccc..9b78b66 100644 --- a/test/ide/claude_adapter_test.dart +++ b/test/ide/claude_adapter_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/claude_adapter.dart'; @@ -7,6 +8,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a ClaudeAdapter', () { late ClaudeAdapter adapter; diff --git a/test/ide/cline_adapter_test.dart b/test/ide/cline_adapter_test.dart index e504d97..2977705 100644 --- a/test/ide/cline_adapter_test.dart +++ b/test/ide/cline_adapter_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/cline_adapter.dart'; @@ -7,6 +8,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a ClineAdapter', () { late ClineAdapter adapter; diff --git a/test/ide/copilot_adapter_test.dart b/test/ide/copilot_adapter_test.dart index d198dcf..02d5b78 100644 --- a/test/ide/copilot_adapter_test.dart +++ b/test/ide/copilot_adapter_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/copilot_adapter.dart'; @@ -7,6 +8,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a CopilotAdapter', () { late CopilotAdapter adapter; diff --git a/test/ide/cursor_adapter_test.dart b/test/ide/cursor_adapter_test.dart index c2b3f20..37f1ea7 100644 --- a/test/ide/cursor_adapter_test.dart +++ b/test/ide/cursor_adapter_test.dart @@ -1,11 +1,16 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/cursor_adapter.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a CursorAdapter', () { late CursorAdapter adapter; diff --git a/test/ide/generic_adapter_test.dart b/test/ide/generic_adapter_test.dart index e5a3f1b..4850810 100644 --- a/test/ide/generic_adapter_test.dart +++ b/test/ide/generic_adapter_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/generic_adapter.dart'; import 'package:skills/src/models/skill_manifest.dart'; @@ -9,6 +10,9 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import '../fake_dialog_support.dart'; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); group('Given a GenericAdapter', () { late GenericAdapter adapter; late FakeDialogSupport fakeDialogSupport; diff --git a/test/ide/ide_detection_test.dart b/test/ide/ide_detection_test.dart index a8c5015..7ec3b33 100644 --- a/test/ide/ide_detection_test.dart +++ b/test/ide/ide_detection_test.dart @@ -1,8 +1,13 @@ +import 'package:logging/logging.dart'; import 'package:skills/src/ide/ide.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a project with a .cursor directory', () { test('when detecting IDE then returns cursor', () async { await d.dir('cursor_project', [d.dir('.cursor')]).create(); diff --git a/test/ide/opencode_adapter_test.dart b/test/ide/opencode_adapter_test.dart index 5e9387f..7a26e21 100644 --- a/test/ide/opencode_adapter_test.dart +++ b/test/ide/opencode_adapter_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/adapters/opencode_adapter.dart'; @@ -7,6 +8,10 @@ import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given an OpenCodeAdapter', () { late OpenCodeAdapter adapter; diff --git a/test/integration/multi_ide_lifecycle_test.dart b/test/integration/multi_ide_lifecycle_test.dart index 1610faa..655461b 100644 --- a/test/integration/multi_ide_lifecycle_test.dart +++ b/test/integration/multi_ide_lifecycle_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/core/skill_installer.dart'; import 'package:skills/src/core/skill_scanner.dart'; import 'package:skills/src/ide/ide.dart'; @@ -13,6 +14,10 @@ void main() { late List pkgBSkills; late FakeDialogSupport fakeDialogSupport; + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + setUp(() async { fakeDialogSupport = FakeDialogSupport(); // Source packages with skills. diff --git a/test/models/skill_manifest_test.dart b/test/models/skill_manifest_test.dart index 05743c6..1e96f21 100644 --- a/test/models/skill_manifest_test.dart +++ b/test/models/skill_manifest_test.dart @@ -1,10 +1,15 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/models/skill_manifest.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a SkillManifest', () { test('when serializing and deserializing then round-trips correctly', () { final manifest = SkillManifest( diff --git a/test/models/skill_metadata_test.dart b/test/models/skill_metadata_test.dart index 0f7b8f4..e1deeb5 100644 --- a/test/models/skill_metadata_test.dart +++ b/test/models/skill_metadata_test.dart @@ -1,10 +1,15 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/models/skill_metadata.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('Given a valid SKILL.md with name and description', () { test('when parsing then returns correct metadata', () async { final content = ''' From 14933222b9a6312487e607a47e3df07cc99a8d4a Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Tue, 12 May 2026 15:51:05 +0000 Subject: [PATCH 08/13] fix test for windows and simplify package config loading --- lib/src/core/package_resolver.dart | 5 +---- test/commands/get_command_select_packages_test.dart | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/core/package_resolver.dart b/lib/src/core/package_resolver.dart index 84b3ace..d89446f 100644 --- a/lib/src/core/package_resolver.dart +++ b/lib/src/core/package_resolver.dart @@ -84,10 +84,7 @@ class PackageResolver { final configFile = File(configPath); if (!configFile.existsSync()) continue; - final configDir = Directory(p.dirname(p.dirname(configPath))); - final config = await findPackageConfig(configDir); - if (config == null) continue; - + final config = await loadPackageConfig(configFile); for (final package in config.packages) { if (memberNames.contains(package.name)) continue; if (seen.contains(package.name)) continue; diff --git a/test/commands/get_command_select_packages_test.dart b/test/commands/get_command_select_packages_test.dart index 5fb9fab..bdc9eac 100644 --- a/test/commands/get_command_select_packages_test.dart +++ b/test/commands/get_command_select_packages_test.dart @@ -60,12 +60,12 @@ environment: {'name': 'test_app', 'rootUri': '../', 'packageUri': 'lib/'}, { 'name': 'dep1', - 'rootUri': dep1Dir.io.path, + 'rootUri': dep1Dir.io.uri.toString(), 'packageUri': 'lib/' }, { 'name': 'dep2', - 'rootUri': dep2Dir.io.path, + 'rootUri': dep2Dir.io.uri.toString(), 'packageUri': 'lib/' }, ], From 24546b4625a5a2e97b3104e389c98b5edd87678b Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 22 May 2026 21:06:38 +0000 Subject: [PATCH 09/13] remove cruft config --- .jetskicli/project.json | 1 - 1 file changed, 1 deletion(-) delete mode 120000 .jetskicli/project.json diff --git a/.jetskicli/project.json b/.jetskicli/project.json deleted file mode 120000 index 1683aa6..0000000 --- a/.jetskicli/project.json +++ /dev/null @@ -1 +0,0 @@ -/usr/local/google/home/jakemac/.gemini/config/projects/993a0563-83ff-4bbb-8123-4ba4961c952e.json \ No newline at end of file From 34040539154da7af94fdfc4b98dfbb57fd16b5be Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 22 May 2026 21:20:54 +0000 Subject: [PATCH 10/13] code review comments --- lib/src/commands/get_skills.dart | 22 +++++++++++++++++++--- lib/src/commands/remove_command.dart | 6 ++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/src/commands/get_skills.dart b/lib/src/commands/get_skills.dart index 02af826..c9c4ff1 100644 --- a/lib/src/commands/get_skills.dart +++ b/lib/src/commands/get_skills.dart @@ -16,6 +16,8 @@ import 'package:skills/src/core/dialog_support.dart'; import '../models/skill_manifest.dart'; /// Installs skills from package dependencies for [ides]. +/// +/// Returns `true` on success or `false` otherwise. Future getSkills({ required List ides, required Logger logger, @@ -35,9 +37,20 @@ Future getSkills({ packageNames: packageNames, ); - if (packageNames != null && packages.isEmpty) { - logger.severe('None of the requested packages were found in dependencies.'); - return false; + if (packageNames != null) { + if (packages.isEmpty) { + logger + .severe('None of the requested packages were found in dependencies.'); + return false; + } + + final foundNames = packages.map((p) => p.name).toSet(); + final missing = packageNames.difference(foundNames)..remove('all'); + if (missing.isNotEmpty) { + logger.warning( + 'Warning: The following requested packages were not found in ' + 'dependencies: ${missing.join(', ')}'); + } } final scanner = SkillScanner(logger); @@ -84,6 +97,9 @@ Future getSkills({ final selectedPackages = selectedIndices.map((i) => packagesWithSkills[i]).toSet(); skills.removeWhere((s) => !selectedPackages.contains(s.packageName)); + } else { + logger.info('Installation aborted by user.'); + return false; } } else { logger.info('Available packages with skills:'); diff --git a/lib/src/commands/remove_command.dart b/lib/src/commands/remove_command.dart index a11f1eb..7af5f3e 100644 --- a/lib/src/commands/remove_command.dart +++ b/lib/src/commands/remove_command.dart @@ -113,8 +113,10 @@ class RemoveCommand extends SkillsCommand { await manifest.save(manifestFile(rootPath)); } - if (packagesToRemove.isNotEmpty) { - logger.info('Removed skills from ${packagesToRemove.join(', ')}.'); + if (totalRemoved > 0) { + logger.info('Removed $totalRemoved skill(s) from ' + '${packagesToRemove.join(', ')}.'); + } else { logger.info('Removed $totalRemoved managed skill(s).'); } From 64bcc642fb2054ab27d31f959aca9d8f13f529ca Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 22 May 2026 21:29:49 +0000 Subject: [PATCH 11/13] fix get_command_registry_test --- test/commands/get_command_registry_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/get_command_registry_test.dart b/test/commands/get_command_registry_test.dart index 7be59e9..029b462 100644 --- a/test/commands/get_command_registry_test.dart +++ b/test/commands/get_command_registry_test.dart @@ -71,7 +71,7 @@ environment: final projectPath = p.join(testRootPath, 'project'); final getCommand = GetCommand( - dialogSupport: FakeDialogSupport(), + dialogSupport: FakeDialogSupport()..multiSelectResult = {0}, gitRunner: GitRunner(isAvailableOverride: _gitUnavailable), ); final runner = CommandRunner('skills', 'Test') From bc46071073aa5c0cdecdf255fb1cd9874503d4e4 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 22 May 2026 21:30:49 +0000 Subject: [PATCH 12/13] format --- lib/src/commands/remove_command.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/commands/remove_command.dart b/lib/src/commands/remove_command.dart index 7af5f3e..310fc31 100644 --- a/lib/src/commands/remove_command.dart +++ b/lib/src/commands/remove_command.dart @@ -116,7 +116,6 @@ class RemoveCommand extends SkillsCommand { if (totalRemoved > 0) { logger.info('Removed $totalRemoved skill(s) from ' '${packagesToRemove.join(', ')}.'); - } else { logger.info('Removed $totalRemoved managed skill(s).'); } From cdedb5633bf6762c14fc1d54aff1373b2cb2aedf Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 22 May 2026 21:33:07 +0000 Subject: [PATCH 13/13] remove unused import after merge --- lib/src/core/package_resolver.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/core/package_resolver.dart b/lib/src/core/package_resolver.dart index f9c927a..e2e73f7 100644 --- a/lib/src/core/package_resolver.dart +++ b/lib/src/core/package_resolver.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:package_config/package_config.dart'; -import 'package:path/path.dart' as p; import 'workspace_resolver.dart';