diff --git a/CHANGELOG.md b/CHANGELOG.md index db732a3..e4035ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.4.0-dev +- 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. diff --git a/lib/src/commands/get_command.dart b/lib/src/commands/get_command.dart index 2df797b..a4b1421 100644 --- a/lib/src/commands/get_command.dart +++ b/lib/src/commands/get_command.dart @@ -47,7 +47,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 f664737..40cbbbf 100644 --- a/lib/src/commands/get_skills.dart +++ b/lib/src/commands/get_skills.dart @@ -21,6 +21,8 @@ import 'package:skills/src/models/global_config.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, @@ -28,7 +30,7 @@ Future getSkills({ DialogSupport? dialogSupport, GitRunner gitRunner = const GitRunner(), String usage = '', - String? packageName, + Set? packageNames, }) async { final ready = await PubRunner.ensureWorkspaceConfigs(workspace); if (!ready) { @@ -37,12 +39,23 @@ 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.'); - 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 rootPath = workspace.rootPath; @@ -109,14 +122,51 @@ Future getSkills({ final dartSkills = await scanner.scan(packages); 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('Installation aborted by user.'); + return false; + } + } 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 457a2b2..9c49402 100644 --- a/lib/src/commands/prune_command.dart +++ b/lib/src/commands/prune_command.dart @@ -65,28 +65,26 @@ 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}'); } } + await manifest.save(manifestFile(rootPath)); if (manifest.isEmpty) { await SkillManifest.cleanup(rootPath); - } else { - await manifest.save(manifestFile(rootPath)); } if (totalRemoved == 0) { diff --git a/lib/src/commands/remove_command.dart b/lib/src/commands/remove_command.dart index f5cb24f..e04bfc7 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; + 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, - packageName: packageName, + packageNames: packagesToRemove, ); manifest = result.manifest; totalRemoved += result.removedCount; @@ -67,14 +107,14 @@ class RemoveCommand extends SkillsCommand { } } + await manifest.save(manifestFile(rootPath)); if (manifest.isEmpty) { await SkillManifest.cleanup(rootPath); - } else { - await manifest.save(manifestFile(rootPath)); } - if (packageName != null) { - logger.info('Removed skills from $packageName.'); + if (totalRemoved > 0) { + logger.info('Removed $totalRemoved skill(s) from ' + '${packagesToRemove.join(', ')}.'); } else { logger.info('Removed $totalRemoved managed skill(s).'); } diff --git a/lib/src/commands/skills_command.dart b/lib/src/commands/skills_command.dart index 1e34bb5..8e55dc0 100644 --- a/lib/src/commands/skills_command.dart +++ b/lib/src/commands/skills_command.dart @@ -22,11 +22,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 8c47afd..da88145 100644 --- a/lib/src/core/cli_dialog_support.dart +++ b/lib/src/core/cli_dialog_support.dart @@ -31,8 +31,11 @@ class CliUtilDialogSupport implements DialogSupport { } @override - Future?> showMultiSelectDialog(List options, - {String? title}) async { + Future?> showMultiSelectDialog( + List options, { + String? title, + Set initialSelected = const {}, + }) async { if (title != null) io.stdout.writeln(title); final result = await cli.showMultiSelectDialog(options, _sharedStdIn); if (result != null) { 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 9a2c8b4..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'; @@ -58,10 +57,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(); @@ -76,14 +76,16 @@ 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; - 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 2d774dd..9f90945 100644 --- a/lib/src/core/skill_installer.dart +++ b/lib/src/core/skill_installer.dart @@ -183,40 +183,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( @@ -225,7 +212,7 @@ class SkillInstaller { } } return SkillRemoveResult( - manifest: manifest.withoutIde(ide.cliName), + manifest: manifest, removedCount: removed.length, removed: removed, ); diff --git a/test/commands/get_command_registry_test.dart b/test/commands/get_command_registry_test.dart index fe41b72..ce6e3d6 100644 --- a/test/commands/get_command_registry_test.dart +++ b/test/commands/get_command_registry_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/commands/get_command.dart'; import 'package:skills/src/commands/skills_command_runner.dart'; @@ -13,6 +14,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', @@ -68,7 +73,7 @@ environment: final projectPath = p.join(testRootPath, 'project'); final getCommand = GetCommand( - dialogSupport: FakeDialogSupport(), + dialogSupport: FakeDialogSupport()..multiSelectResult = {0}, gitRunner: GitRunner(isAvailableOverride: _gitUnavailable), ); final runner = SkillsCommandRunner('skills', 'Test') @@ -136,13 +141,13 @@ environment: GlobalConfig.globalPathOverride = globalConfigPath; addTearDown(() => GlobalConfig.globalPathOverride = null); - final fileUrl = '../mock_registry'; var globalConfig = const GlobalConfig(); - globalConfig = globalConfig.withRegistry(RegistryRepo(cloneUrl: fileUrl)); + globalConfig = + globalConfig.withRegistry(RegistryRepo(cloneUrl: registryPath)); await globalConfig.save(File(globalConfigPath)); final getCommand = GetCommand( - dialogSupport: FakeDialogSupport(), + dialogSupport: FakeDialogSupport()..multiSelectResult = {0}, ); final runner = SkillsCommandRunner('skills', 'Test') @@ -150,15 +155,12 @@ environment: await runner.run(['--directory', projectPath, 'get', '--ide', 'cursor']); - expect( - Directory(p.join(projectPath, '.cursor', 'skills', 'pkg-skill')) - .existsSync(), - isTrue); + await d.dir(projectPath, [d.dir('.cursor/skills/pkg-skill')]).validate(); final updatedGlobalConfig = await GlobalConfig.loadOrEmpty(File(globalConfigPath)); final repo = updatedGlobalConfig.registries - .firstWhere((r) => r.cloneUrl == fileUrl); + .firstWhere((r) => r.cloneUrl == registryPath); expect(repo.installs, isNotEmpty); expect(repo.installs.first, contains('pkg-skill')); }); 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..f89bfb0 --- /dev/null +++ b/test/commands/get_command_select_packages_test.dart @@ -0,0 +1,213 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; +import 'package:skills/src/commands/get_command.dart'; +import 'package:skills/src/commands/skills_command_runner.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; + + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + + 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.uri.toString(), + 'packageUri': 'lib/' + }, + { + 'name': 'dep2', + 'rootUri': dep2Dir.io.uri.toString(), + '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 then only ' + 'dep1 should be installed', () async { + fakeDialogSupport.multiSelectResult = {0}; + final getCommand = GetCommand( + dialogSupport: fakeDialogSupport, + gitRunner: + GitRunner(isAvailableOverride: () async => false), // skip registry + ); + final runner = SkillsCommandRunner('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); + expect(await dep2SkillDir.exists(), isFalse); + + 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')); + expect(skillNames, isNot(contains('dep2-skill'))); + }); + + 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), + ); + final runner = SkillsCommandRunner('skills', 'Test') + ..addCommand(getCommand); + + await runner.run([ + 'get', + '--directory', + projectPath, + '--ide', + Ide.generic.cliName, + '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 all` (non-interactive) then all skills ' + 'should be installed', () async { + final getCommand = GetCommand( + dialogSupport: null, + gitRunner: GitRunner(isAvailableOverride: () async => false), + ); + final runner = SkillsCommandRunner('skills', 'Test') + ..addCommand(getCommand); + + await runner.run([ + 'get', + '--directory', + projectPath, + '--ide', + Ide.generic.cliName, + '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 package arguments and NO dialog ' + 'support then no skills should be installed', () async { + final getCommand = GetCommand( + dialogSupport: null, + gitRunner: GitRunner(isAvailableOverride: () async => false), + ); + final runner = SkillsCommandRunner('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/commands/get_command_test.dart b/test/commands/get_command_test.dart index 24f73e6..13dbdad 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 e5b841c..c25193f 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 9816504..261071f 100644 --- a/test/commands/prune_command_test.dart +++ b/test/commands/prune_command_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:skills/src/commands/skills_command_runner.dart'; import 'package:path/path.dart' as p; import 'package:skills/src/commands/prune_command.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/registry_command_test.dart b/test/commands/registry_command_test.dart index 372ea49..f9adba2 100644 --- a/test/commands/registry_command_test.dart +++ b/test/commands/registry_command_test.dart @@ -1,5 +1,4 @@ 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/registry_command.dart'; @@ -16,7 +15,7 @@ void main() { late String projectPath; late String globalConfigPath; late FakeDialogSupport fakeDialogSupport; - late CommandRunner runner; + late SkillsCommandRunner runner; setUp(() async { await d.dir('project', [ diff --git a/test/commands/remove_command_test.dart b/test/commands/remove_command_test.dart index 7db3e78..fd805e9 100644 --- a/test/commands/remove_command_test.dart +++ b/test/commands/remove_command_test.dart @@ -1,64 +1,85 @@ +import 'dart:convert'; import 'dart:io'; -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:args/command_runner.dart'; +import 'package:logging/logging.dart'; +import 'package:skills/src/commands/remove_command.dart'; +import 'package:skills/src/commands/skills_command_runner.dart'; import 'package:skills/src/models/skill_manifest.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; +import '../fake_dialog_support.dart'; + void main() { - group('Given a project with managed skills installed', () { + late CommandRunner runner; + late FakeDialogSupport fakeDialogSupport; + + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + + setUp(() { + fakeDialogSupport = FakeDialogSupport(); + final removeCommand = RemoveCommand(dialogSupport: fakeDialogSupport); + runner = SkillsCommandRunner('skills', 'test')..addCommand(removeCommand); + }); + + group('Given a project with installed skills for dep1 and dep2', () { late String projectPath; late SkillManifest manifest; 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-1', [d.file('SKILL.md', 'content')]), + d.dir('dep1-skill-2', [d.file('SKILL.md', 'content')]), + d.dir('dep2-skill-3', [d.file('SKILL.md', 'content')]), ]), ]), - ]).create(); + ]); + await projectRootDir.create(); - projectPath = d.path('project'); + projectPath = projectRootDir.io.path; manifest = SkillManifest( installations: { 'cursor': { - 'pkg_a': PackageSkillsEntry( + 'dep1': PackageSkillsEntry( skills: [ InstalledSkillEntry( - name: 'pkg_a-skill-1', + name: 'dep1-skill-1', installedAt: DateTime.utc(2026), ), InstalledSkillEntry( - name: 'pkg_a-skill-2', + name: 'dep1-skill-2', installedAt: DateTime.utc(2026), ), ], ), - 'pkg_b': PackageSkillsEntry( + 'dep2': PackageSkillsEntry( skills: [ InstalledSkillEntry( - name: 'pkg_b-skill-3', + name: 'dep2-skill-3', installedAt: DateTime.utc(2026), ), ], @@ -70,73 +91,57 @@ void main() { 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 running `skills remove dep1` then removes only dep1 skills', + () async { + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'cursor', 'dep1']); - test('when removing all then all managed skills are removed', () async { - final adapter = CursorAdapter(projectPath); + await d.dir('project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill-1'), + d.nothing('dep1-skill-2'), + d.dir('dep2-skill-3'), + ]), + ]), + d.dir(SkillManifest.configDirPath, [ + d.file( + 'skills_config.json', + allOf( + isNot(contains('dep1-skill-1')), + isNot(contains('dep1-skill-2')), + contains('dep2-skill-3'), + ), + ), + ]), + ]).validate(); + }); - for (final entry in manifest.packagesForIde('cursor').values) { - for (final skill in entry.skills) { - await adapter.removeSkill(skill.name); - } - } + test('when running `skills remove all` then removes all skills', () async { + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'cursor', 'all']); - expect( - await Directory('$projectPath/.cursor/skills/pkg_a-skill-1').exists(), - isFalse, - ); - expect( - await Directory('$projectPath/.cursor/skills/pkg_b-skill-3').exists(), - isFalse, - ); + await d.dir('project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.nothing('dep2-skill'), + ]), + ]), + d.dir('.dart_tool', [d.nothing('skills')]), + ]).validate(); }); test( 'when removing all then cache and config directories are 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.configDirPath)); - expect(await dartSkillsDir.exists(), isTrue); - - final cacheDir = - Directory(p.join(projectPath, SkillManifest.cacheDirPath)); - await cacheDir.create(recursive: true); - await updated.save(File(SkillManifest.pathIn(projectPath))); - await SkillManifest.cleanup(projectPath); - - expect(await dartSkillsDir.exists(), isFalse); - expect(await cacheDir.exists(), isFalse); + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'cursor', 'all']); + + await d.dir('project', [ + d.nothing(SkillManifest.configDirPath), + d.nothing(SkillManifest.cacheDirPath) + ]).validate(); }, ); }); @@ -144,8 +149,17 @@ void main() { group('Given a project with no managed skills', () { test('when removing then manifest remains empty', () async { await d.dir('empty_project', [ + d.file('pubspec.yaml', ''' +name: test_app +environment: + sdk: ^3.0.0 +'''), d.dir('.cursor', [d.dir('skills')]), ]).create(); + var projectPath = d.dir('empty_project').io.path; + + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'cursor', 'all']); final manifest = await SkillManifest.loadFromRoot(d.path('empty_project')); @@ -160,22 +174,39 @@ void main() { setUp(() async { await d.dir('multi_project', [ + d.file('pubspec.yaml', ''' +name: test_app +environment: + sdk: ^3.0.0 +'''), d.dir('.cursor', [ d.dir('skills', [ - d.dir('pkg-skill-a', [ + d.dir('dep1-skill', [ d.file( 'SKILL.md', - '---\nname: pkg-skill-a\ndescription: a\n---\nBody A', + '---\nname: pkg-skill\ndescription: a\n---\nBody A', + ), + ]), + d.dir('dep2-skill', [ + d.file( + 'SKILL.md', + '---\nname: dep2-skill\ndescription: a\n---\nBody A', ), ]), ]), ]), d.dir('.claude', [ d.dir('skills', [ - d.dir('pkg-skill-a', [ + d.dir('dep1-skill', [ d.file( 'SKILL.md', - '---\nname: pkg-skill-a\ndescription: a\n---\nBody A', + '---\nname: dep1-skill\ndescription: a\n---\nBody A', + ), + ]), + d.dir('dep2-skill', [ + d.file( + 'SKILL.md', + '---\nname: dep2-skill\ndescription: a\n---\nBody A', ), ]), ]), @@ -184,28 +215,26 @@ void main() { projectPath = d.path('multi_project'); + final dep1SkillsEntry = PackageSkillsEntry( + skills: [ + InstalledSkillEntry( + name: 'dep1-skill', + installedAt: DateTime.utc(2026), + ), + ], + ); + final dep2SkillsEntry = PackageSkillsEntry( + skills: [ + InstalledSkillEntry( + name: 'dep2-skill', + installedAt: DateTime.utc(2026), + ), + ], + ); 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), - ), - ], - ), - }, + 'cursor': {'dep1': dep1SkillsEntry, 'dep2': dep2SkillsEntry}, + 'claude': {'dep1': dep1SkillsEntry, 'dep2': dep2SkillsEntry}, }, ); @@ -213,68 +242,90 @@ void main() { }); test( - 'when removing one IDE then files for that IDE are deleted and ' - 'other IDE files remain', () async { - final cursorAdapter = CursorAdapter(projectPath); - - 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)); + 'when running `skills remove` without arguments removes the' + 'selected skills for all IDEs', () async { + fakeDialogSupport.multiSelectResult = {0}; + + await runner.run(['remove', '--directory', projectPath]); + + await d.dir('multi_project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + d.dir('.claude', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + ]).validate(); }); 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'); - - for (final skill in updated.packagesForIde('claude')['pkg']!.skills) { - await claudeAdapter.removeSkill(skill.name); - } - updated = updated.withoutIde('claude'); - - expect( - Directory('$projectPath/.cursor/skills/pkg-skill-a').existsSync(), - isFalse, - ); - expect( - Directory('$projectPath/.claude/skills/pkg-skill-a').existsSync(), - isFalse, - ); - expect(updated.isEmpty, isTrue); + 'when running `skills remove --ide cursor` removes the selected skills' + 'skills for just cursor', () async { + fakeDialogSupport.multiSelectResult = {0}; + + await runner + .run(['remove', '--directory', projectPath, '--ide', 'cursor']); + + await d.dir('multi_project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.nothing('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + d.dir('.claude', [ + d.dir('skills', [ + d.dir('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + ]).validate(); + }); + + test( + 'when running `skills remove` without arguments and NO dialog support ' + 'then does nothing and prints packages', () async { + await runner + .run(['remove', '--directory', projectPath, '--ide', 'cursor']); + + await d.dir('multi_project', [ + d.dir('.cursor', [ + d.dir('skills', [ + d.dir('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + d.dir('.claude', [ + d.dir('skills', [ + d.dir('dep1-skill'), + d.dir('dep2-skill'), + ]), + ]), + d.dir(SkillManifest.configDirPath, [ + d.file('skills_config.json', + allOf(contains('dep1-skill'), contains('dep2-skill'))), + ]), + ]).validate(); }); test( 'when Claude skill directory is manually deleted then remove still ' 'cleans manifest without error', () async { Directory( - '$projectPath/.claude/skills/pkg-skill-a', + '$projectPath/.claude/skills/dep1-skill', ).deleteSync(recursive: true); - final claudeAdapter = ClaudeAdapter(projectPath); - for (final skill in manifest.packagesForIde('claude')['pkg']!.skills) { - await claudeAdapter.removeSkill(skill.name); - } - final updated = manifest.withoutIde('claude'); - - expect(updated.allIdes, equals(['cursor'])); + await runner.run( + ['remove', '--directory', projectPath, '--ide', 'claude', 'dep1']); + final manifest = await SkillManifest.loadFromRoot(projectPath); + expect(manifest!.packagesForIde('claude').keys, + allOf(contains('dep2'), isNot(contains('dep1')))); }); }); } 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 c986a04..f593ce3 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 4465ef1..7e8401d 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/core/registry_repos.dart'; import 'package:skills/src/models/skill_manifest.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + Logger.root.onRecord.listen((r) => printOnFailure(r.toString())); + }); + group('RegistryRepo', () { test('pathSegment encodes cloneUrl', () { const repo = RegistryRepo( diff --git a/test/core/registry_scanner_test.dart b/test/core/registry_scanner_test.dart index ddd2a13..a068a68 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 2821f2e..a20e4ff 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 5c9419f..debecb5 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'; @@ -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('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/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/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 f4a3414..fa241ec 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'; @@ -14,6 +15,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. @@ -206,7 +211,7 @@ Instructions for debugging. ide: ide, rootPath: rootPath, manifest: manifest, - packageName: 'pkg_a', + packageNames: {'pkg_a'}, ); manifest = result.manifest; } diff --git a/test/models/skill_manifest_test.dart b/test/models/skill_manifest_test.dart index 0ff1b20..f78116f 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 = '''