From af72d8510a0f565f5232317c020f18c3abf04ff8 Mon Sep 17 00:00:00 2001 From: faithoflifedev Date: Mon, 30 Mar 2026 13:15:26 -0400 Subject: [PATCH 1/3] feat(skill_scanner): accept dash-normalized package name prefix in skill names --- lib/src/core/skill_scanner.dart | 6 ++-- test/core/skill_scanner_test.dart | 53 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/lib/src/core/skill_scanner.dart b/lib/src/core/skill_scanner.dart index 0eec5d8..b904d83 100644 --- a/lib/src/core/skill_scanner.dart +++ b/lib/src/core/skill_scanner.dart @@ -43,6 +43,7 @@ class SkillScanner { if (!await skillsDir.exists()) return []; final prefix = '${package.name}-'; + final prefixWithDashes = '${package.name.replaceAll('_', '-')}-'; final skills = []; await for (final entity in skillsDir.list()) { @@ -53,10 +54,11 @@ class SkillScanner { final skillMdFile = File(p.join(entity.path, 'SKILL.md')); if (!await skillMdFile.exists()) continue; - if (!skillName.startsWith(prefix)) { + if (!(skillName.startsWith(prefix) || + skillName.startsWith(prefixWithDashes))) { stderr.writeln( 'Warning: Skipping skill "$skillName" in ${package.name} ' - '-- name must start with "${package.name}-"', + '-- name must start with "$prefix" or "$prefixWithDashes"', ); continue; } diff --git a/test/core/skill_scanner_test.dart b/test/core/skill_scanner_test.dart index 707e6f5..7151183 100644 --- a/test/core/skill_scanner_test.dart +++ b/test/core/skill_scanner_test.dart @@ -107,6 +107,59 @@ Body. }, ); + group( + 'Given a package with underscores in its name and dash-normalized skill names', + () { + test('when scanning then accepts dash-normalized prefix', () async { + await d.dir('my_cool_pkg', [ + d.dir('skills', [ + d.dir('my-cool-pkg-code-gen', [ + d.file('SKILL.md', ''' +--- +name: my-cool-pkg-code-gen +description: Generates code. +--- +Body. +'''), + ]), + d.dir('my_cool_pkg-api-design', [ + d.file('SKILL.md', ''' +--- +name: my_cool_pkg-api-design +description: Designs APIs. +--- +Body. +'''), + ]), + d.dir('wrong-prefix-skill', [ + d.file('SKILL.md', ''' +--- +name: wrong-prefix-skill +description: Invalid. +--- +Body. +'''), + ]), + ]), + ]).create(); + + final package = ResolvedPackage( + name: 'my_cool_pkg', + rootPath: d.path('my_cool_pkg'), + ); + + const scanner = SkillScanner(); + final skills = await scanner.scanPackage(package); + + expect(skills, hasLength(2)); + expect( + skills.map((s) => s.skillName).toSet(), + equals({'my-cool-pkg-code-gen', 'my_cool_pkg-api-design'}), + ); + }); + }, + ); + group('Given a package without a skills directory', () { test('when scanning then returns empty list', () async { await d.dir('no_skills_package', [ From ef1bae0996511ebbc6928a36d6ed0766edec6fde Mon Sep 17 00:00:00 2001 From: faithoflifedev Date: Wed, 1 Apr 2026 09:46:56 -0400 Subject: [PATCH 2/3] feat: add Qwen Code IDE support - Add Ide.qwen enum value with .qwen/skills install path - Add QwenAdapter extending AgentSkillsAdapter - Add IDE detection for .qwen directory marker - Add tests for QwenAdapter and IDE detection - Update README with Qwen Code in supported IDEs table - Update AGENTS.md with Qwen install location Usage: skills get --ide qwen Auto-detected when .qwen/ directory exists in project. Co-authored-by: Qwen-Coder --- AGENTS.md | 2 +- README.md | 1 + lib/src/ide/adapters/qwen_adapter.dart | 10 ++ lib/src/ide/ide.dart | 4 +- lib/src/ide/ide_adapter_factory.dart | 2 + test/ide/ide_detection_test.dart | 12 +++ test/ide/qwen_adapter_test.dart | 127 +++++++++++++++++++++++++ 7 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 lib/src/ide/adapters/qwen_adapter.dart create mode 100644 test/ide/qwen_adapter_test.dart diff --git a/AGENTS.md b/AGENTS.md index a4122a7..3436fae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Fix any format, analysis, or test failures before finishing. - **Paths:** Use `package:path/path.dart` and `p.join()` for all path construction so behavior is correct on Windows. - **Empty manifest:** When all managed skills are removed, delete the `.dart_skills` directory rather than leaving an empty manifest file (see `SkillManifest.cleanupDir` and `RemoveCommand`). - **Workspace resolution:** The CLI supports (1) a directory with a `pubspec.yaml` (pub workspace, melos, or single package) and (2) an implicit workspace: no root `pubspec.yaml`, but immediate subdirectories that have `pubspec.yaml` are treated as packages. Do not walk up the directory tree to find a project root; the user is expected to run from the project root. -- **IDE install locations:** Install full Agent Skills (SKILL.md plus scripts, references, assets) into each IDE’s documented location: `.cursor/skills/`, `.agent/skills/`, `.claude/skills/`, `.cline/skills/`, `.github/skills/`. See README for spec links. +- **IDE install locations:** Install full Agent Skills (SKILL.md plus scripts, references, assets) into each IDE's documented location: `.cursor/skills/`, `.agent/skills/`, `.claude/skills/`, `.cline/skills/`, `.github/skills/`, `.qwen/skills/`. See README for spec links. - **Registry repos:** GitHub registry repos are cloned/updated under `.dart_skills/repos//`. The merge step gives Dart-package skills precedence per package: if a dependency ships its own skills, registry skills for that package are not installed. - **Generic IDE:** Antigravity, Codex, and generic are separate CLI options that all install to `.agent/skills/`. Only `generic` is stored in `skills_config.json`. - **Listing IDEs:** When listing agents/IDEs (docs, help text, CLI options), use alphabetical order with generic last. diff --git a/README.md b/README.md index 7cc485f..998a94f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ The CLI auto-detects your IDE from project directory markers. If multiple IDEs a | [Cursor](https://cursor.com/docs/skills) | `--ide cursor` | `.cursor/skills/` | Agent Skills | | [GitHub Copilot](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills) | `--ide copilot` | `.github/skills/` | Agent Skills | | Generic | `--ide generic` | `.agent/skills/` | Agent Skills | +| [Qwen Code](https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/) | `--ide qwen` | `.qwen/skills/` | Agent Skills | Antigravity, Codex, and generic all install to the same `.agent/skills/` directory (only `generic` is stored in the config). GitHub Copilot is not auto-detected (`.github/` is often used for other purposes); use `--ide copilot` to install skills for Copilot explicitly. diff --git a/lib/src/ide/adapters/qwen_adapter.dart b/lib/src/ide/adapters/qwen_adapter.dart new file mode 100644 index 0000000..83f1723 --- /dev/null +++ b/lib/src/ide/adapters/qwen_adapter.dart @@ -0,0 +1,10 @@ +import '../ide.dart'; +import 'agent_skills_adapter.dart'; + +/// Qwen Code adapter. +/// +/// Installs skills to `.qwen/skills//` per +/// [Qwen Code skills](https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/). +class QwenAdapter extends AgentSkillsAdapter { + QwenAdapter(String projectPath) : super(Ide.qwen.skillsPath(projectPath)); +} diff --git a/lib/src/ide/ide.dart b/lib/src/ide/ide.dart index bc86093..b1367be 100644 --- a/lib/src/ide/ide.dart +++ b/lib/src/ide/ide.dart @@ -12,7 +12,8 @@ enum Ide { generic('generic', '.agent/skills'), claude('claude', '.claude/skills'), copilot('copilot', '.github/skills'), - cline('cline', '.cline/skills'); + cline('cline', '.cline/skills'), + qwen('qwen', '.qwen/skills'); final String cliName; @@ -43,6 +44,7 @@ enum Ide { Ide.claude => Directory(p.join(projectPath, '.claude')).existsSync(), Ide.cline => Directory(p.join(projectPath, '.cline')).existsSync() || Directory(p.join(projectPath, '.clinerules')).existsSync(), + Ide.qwen => Directory(p.join(projectPath, '.qwen')).existsSync(), Ide.copilot => false, }; } diff --git a/lib/src/ide/ide_adapter_factory.dart b/lib/src/ide/ide_adapter_factory.dart index 9573466..a8d9c80 100644 --- a/lib/src/ide/ide_adapter_factory.dart +++ b/lib/src/ide/ide_adapter_factory.dart @@ -3,6 +3,7 @@ import 'adapters/cline_adapter.dart'; import 'adapters/copilot_adapter.dart'; import 'adapters/cursor_adapter.dart'; import 'adapters/generic_adapter.dart'; +import 'adapters/qwen_adapter.dart'; import 'ide.dart'; import 'ide_adapter.dart'; @@ -14,5 +15,6 @@ IdeAdapter createIdeAdapter(Ide ide, String projectPath) { Ide.claude => ClaudeAdapter(projectPath), Ide.copilot => CopilotAdapter(projectPath), Ide.cline => ClineAdapter(projectPath), + Ide.qwen => QwenAdapter(projectPath), }; } diff --git a/test/ide/ide_detection_test.dart b/test/ide/ide_detection_test.dart index 63280a0..6c731e3 100644 --- a/test/ide/ide_detection_test.dart +++ b/test/ide/ide_detection_test.dart @@ -47,6 +47,17 @@ void main() { }); }); + group('Given a project with a .qwen directory', () { + test('when detecting IDE then returns qwen', () async { + await d.dir('qwen_project', [d.dir('.qwen')]).create(); + + const detector = IdeDetector(); + final ide = detector.detect(d.path('qwen_project')); + + expect(ide, equals(Ide.qwen)); + }); + }); + group('Given a project with .github/copilot-instructions.md', () { test('when detecting IDE then does not auto-detect copilot', () async { await d.dir('copilot_project', [ @@ -119,6 +130,7 @@ void main() { expect(Ide.fromCliName('claude'), equals(Ide.claude)); expect(Ide.fromCliName('copilot'), equals(Ide.copilot)); expect(Ide.fromCliName('cline'), equals(Ide.cline)); + expect(Ide.fromCliName('qwen'), equals(Ide.qwen)); }); test('when given generic aliases then returns generic', () { diff --git a/test/ide/qwen_adapter_test.dart b/test/ide/qwen_adapter_test.dart new file mode 100644 index 0000000..1b3b50e --- /dev/null +++ b/test/ide/qwen_adapter_test.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +import 'package:skills/src/core/skill_scanner.dart'; +import 'package:skills/src/ide/adapters/qwen_adapter.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + group('Given a QwenAdapter', () { + late QwenAdapter adapter; + + setUp(() async { + await d.dir('project', [ + d.dir('.qwen', [d.dir('skills')]), + ]).create(); + + adapter = QwenAdapter(d.path('project')); + }); + + group('and a scanned skill with SKILL.md and supporting files', () { + late ScannedSkill skill; + + setUp(() async { + await d.dir('source_pkg', [ + d.dir('skills', [ + d.dir('source_pkg-my-skill', [ + d.file('SKILL.md', ''' +--- +name: source_pkg-my-skill +description: A test skill. +--- + +# My Skill + +Instructions here. +'''), + d.dir('scripts', [d.file('run.sh', '#!/bin/bash\necho hello')]), + d.dir('references', [d.file('guide.md', '# Guide')]), + ]), + ]), + ]).create(); + + skill = ScannedSkill( + packageName: 'source_pkg', + skillName: 'source_pkg-my-skill', + skillPath: d.path('source_pkg/skills/source_pkg-my-skill'), + ); + }); + + test('when installing then creates directory with skill name', () async { + final name = await adapter.installSkill(skill); + + expect(name, equals('source_pkg-my-skill')); + + final installed = Directory( + d.path('project/.qwen/skills/source_pkg-my-skill'), + ); + expect(await installed.exists(), isTrue); + }); + + test('when installing then copies SKILL.md as-is', () async { + await adapter.installSkill(skill); + + final skillMd = File( + d.path('project/.qwen/skills/source_pkg-my-skill/SKILL.md'), + ); + final content = await skillMd.readAsString(); + + expect(content, contains('name: source_pkg-my-skill')); + expect(content, contains('# My Skill')); + }); + + test('when installing then copies supporting directories', () async { + await adapter.installSkill(skill); + + final script = File( + d.path('project/.qwen/skills/source_pkg-my-skill/scripts/run.sh'), + ); + expect(await script.exists(), isTrue); + + final ref = File( + d.path( + 'project/.qwen/skills/source_pkg-my-skill/references/guide.md', + ), + ); + expect(await ref.exists(), isTrue); + }); + + test('when reinstalling then replaces existing skill', () async { + await adapter.installSkill(skill); + await adapter.installSkill(skill); + + final installed = Directory( + d.path('project/.qwen/skills/source_pkg-my-skill'), + ); + expect(await installed.exists(), isTrue); + }); + }); + + group('and a previously installed skill', () { + setUp(() async { + await d.dir('project', [ + d.dir('.qwen', [ + d.dir('skills', [ + d.dir('pkg-old-skill', [ + d.file( + 'SKILL.md', + '---\nname: pkg-old-skill\n' + 'description: old\n---\nOld.', + ), + ]), + ]), + ]), + ]).create(); + }); + + test('when removing then deletes the skill directory', () async { + await adapter.removeSkill('pkg-old-skill'); + + final removed = Directory( + d.path('project/.qwen/skills/pkg-old-skill'), + ); + expect(await removed.exists(), isFalse); + }); + }); + }); +} From b53638ef434a6303caaf05ec1f5358dd2e18ad41 Mon Sep 17 00:00:00 2001 From: faithoflifedev Date: Thu, 2 Apr 2026 08:11:35 -0400 Subject: [PATCH 3/3] feat: add Qoder IDE support - Add Qoder to the IDE enum and auto-detection logic - Implement QoderAdapter for .qoderwork/skills/ installation - Add tests for Qoder IDE detection and adapter behavior - Update documentation (AGENTS.md, README.md) and .gitignore --- .gitignore | 1 + AGENTS.md | 2 +- README.md | 1 + lib/src/ide/adapters/qoder_adapter.dart | 10 ++ lib/src/ide/ide.dart | 2 + lib/src/ide/ide_adapter_factory.dart | 2 + test/ide/ide_detection_test.dart | 12 +++ test/ide/qoder_adapter_test.dart | 128 ++++++++++++++++++++++++ 8 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 lib/src/ide/adapters/qoder_adapter.dart create mode 100644 test/ide/qoder_adapter_test.dart diff --git a/.gitignore b/.gitignore index 0d61342..8de4134 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ # Created by `dart pub` .dart_tool/ BLOG.md +QWEN.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3436fae..56c9abe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Fix any format, analysis, or test failures before finishing. - **Paths:** Use `package:path/path.dart` and `p.join()` for all path construction so behavior is correct on Windows. - **Empty manifest:** When all managed skills are removed, delete the `.dart_skills` directory rather than leaving an empty manifest file (see `SkillManifest.cleanupDir` and `RemoveCommand`). - **Workspace resolution:** The CLI supports (1) a directory with a `pubspec.yaml` (pub workspace, melos, or single package) and (2) an implicit workspace: no root `pubspec.yaml`, but immediate subdirectories that have `pubspec.yaml` are treated as packages. Do not walk up the directory tree to find a project root; the user is expected to run from the project root. -- **IDE install locations:** Install full Agent Skills (SKILL.md plus scripts, references, assets) into each IDE's documented location: `.cursor/skills/`, `.agent/skills/`, `.claude/skills/`, `.cline/skills/`, `.github/skills/`, `.qwen/skills/`. See README for spec links. +- **IDE install locations:** Install full Agent Skills (SKILL.md plus scripts, references, assets) into each IDE's documented location: `.cursor/skills/`, `.agent/skills/`, `.claude/skills/`, `.cline/skills/`, `.github/skills/`, `.qoderwork/skills/`, `.qwen/skills/`. See README for spec links. - **Registry repos:** GitHub registry repos are cloned/updated under `.dart_skills/repos//`. The merge step gives Dart-package skills precedence per package: if a dependency ships its own skills, registry skills for that package are not installed. - **Generic IDE:** Antigravity, Codex, and generic are separate CLI options that all install to `.agent/skills/`. Only `generic` is stored in `skills_config.json`. - **Listing IDEs:** When listing agents/IDEs (docs, help text, CLI options), use alphabetical order with generic last. diff --git a/README.md b/README.md index 998a94f..c029b5f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ The CLI auto-detects your IDE from project directory markers. If multiple IDEs a | [Cursor](https://cursor.com/docs/skills) | `--ide cursor` | `.cursor/skills/` | Agent Skills | | [GitHub Copilot](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills) | `--ide copilot` | `.github/skills/` | Agent Skills | | Generic | `--ide generic` | `.agent/skills/` | Agent Skills | +| [Qoder](https://docs.qoder.com/qoderwork/skills) | `--ide qoder` | `.qoderwork/skills/` | Agent Skills | | [Qwen Code](https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/) | `--ide qwen` | `.qwen/skills/` | Agent Skills | Antigravity, Codex, and generic all install to the same `.agent/skills/` directory (only `generic` is stored in the config). GitHub Copilot is not auto-detected (`.github/` is often used for other purposes); use `--ide copilot` to install skills for Copilot explicitly. diff --git a/lib/src/ide/adapters/qoder_adapter.dart b/lib/src/ide/adapters/qoder_adapter.dart new file mode 100644 index 0000000..b327a53 --- /dev/null +++ b/lib/src/ide/adapters/qoder_adapter.dart @@ -0,0 +1,10 @@ +import '../ide.dart'; +import 'agent_skills_adapter.dart'; + +/// Qoder adapter. +/// +/// Installs skills to `.qoderwork/skills//` per +/// [Qoder skills](https://docs.qoder.com/qoderwork/skills). +class QoderAdapter extends AgentSkillsAdapter { + QoderAdapter(String projectPath) : super(Ide.qoder.skillsPath(projectPath)); +} diff --git a/lib/src/ide/ide.dart b/lib/src/ide/ide.dart index b1367be..a7ef4d9 100644 --- a/lib/src/ide/ide.dart +++ b/lib/src/ide/ide.dart @@ -13,6 +13,7 @@ enum Ide { claude('claude', '.claude/skills'), copilot('copilot', '.github/skills'), cline('cline', '.cline/skills'), + qoder('qoder', '.qoderwork/skills'), qwen('qwen', '.qwen/skills'); final String cliName; @@ -44,6 +45,7 @@ enum Ide { Ide.claude => Directory(p.join(projectPath, '.claude')).existsSync(), Ide.cline => Directory(p.join(projectPath, '.cline')).existsSync() || Directory(p.join(projectPath, '.clinerules')).existsSync(), + Ide.qoder => Directory(p.join(projectPath, '.qoderwork')).existsSync(), Ide.qwen => Directory(p.join(projectPath, '.qwen')).existsSync(), Ide.copilot => false, }; diff --git a/lib/src/ide/ide_adapter_factory.dart b/lib/src/ide/ide_adapter_factory.dart index a8d9c80..0ffcff9 100644 --- a/lib/src/ide/ide_adapter_factory.dart +++ b/lib/src/ide/ide_adapter_factory.dart @@ -3,6 +3,7 @@ import 'adapters/cline_adapter.dart'; import 'adapters/copilot_adapter.dart'; import 'adapters/cursor_adapter.dart'; import 'adapters/generic_adapter.dart'; +import 'adapters/qoder_adapter.dart'; import 'adapters/qwen_adapter.dart'; import 'ide.dart'; import 'ide_adapter.dart'; @@ -15,6 +16,7 @@ IdeAdapter createIdeAdapter(Ide ide, String projectPath) { Ide.claude => ClaudeAdapter(projectPath), Ide.copilot => CopilotAdapter(projectPath), Ide.cline => ClineAdapter(projectPath), + Ide.qoder => QoderAdapter(projectPath), Ide.qwen => QwenAdapter(projectPath), }; } diff --git a/test/ide/ide_detection_test.dart b/test/ide/ide_detection_test.dart index 6c731e3..7db0a0e 100644 --- a/test/ide/ide_detection_test.dart +++ b/test/ide/ide_detection_test.dart @@ -47,6 +47,17 @@ void main() { }); }); + group('Given a project with a .qoderwork directory', () { + test('when detecting IDE then returns qoder', () async { + await d.dir('qoder_project', [d.dir('.qoderwork')]).create(); + + const detector = IdeDetector(); + final ide = detector.detect(d.path('qoder_project')); + + expect(ide, equals(Ide.qoder)); + }); + }); + group('Given a project with a .qwen directory', () { test('when detecting IDE then returns qwen', () async { await d.dir('qwen_project', [d.dir('.qwen')]).create(); @@ -130,6 +141,7 @@ void main() { expect(Ide.fromCliName('claude'), equals(Ide.claude)); expect(Ide.fromCliName('copilot'), equals(Ide.copilot)); expect(Ide.fromCliName('cline'), equals(Ide.cline)); + expect(Ide.fromCliName('qoder'), equals(Ide.qoder)); expect(Ide.fromCliName('qwen'), equals(Ide.qwen)); }); diff --git a/test/ide/qoder_adapter_test.dart b/test/ide/qoder_adapter_test.dart new file mode 100644 index 0000000..efa9655 --- /dev/null +++ b/test/ide/qoder_adapter_test.dart @@ -0,0 +1,128 @@ +import 'dart:io'; + +import 'package:skills/src/core/skill_scanner.dart'; +import 'package:skills/src/ide/adapters/qoder_adapter.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + group('Given a QoderAdapter', () { + late QoderAdapter adapter; + + setUp(() async { + await d.dir('project', [ + d.dir('.qoderwork', [d.dir('skills')]), + ]).create(); + + adapter = QoderAdapter(d.path('project')); + }); + + group('and a scanned skill with SKILL.md and supporting files', () { + late ScannedSkill skill; + + setUp(() async { + await d.dir('source_pkg', [ + d.dir('skills', [ + d.dir('source_pkg-my-skill', [ + d.file('SKILL.md', ''' +--- +name: source_pkg-my-skill +description: A test skill. +--- + +# My Skill + +Instructions here. +'''), + d.dir('scripts', [d.file('run.sh', '#!/bin/bash\necho hello')]), + d.dir('references', [d.file('guide.md', '# Guide')]), + ]), + ]), + ]).create(); + + skill = ScannedSkill( + packageName: 'source_pkg', + skillName: 'source_pkg-my-skill', + skillPath: d.path('source_pkg/skills/source_pkg-my-skill'), + ); + }); + + test('when installing then creates directory with skill name', () async { + final name = await adapter.installSkill(skill); + + expect(name, equals('source_pkg-my-skill')); + + final installed = Directory( + d.path('project/.qoderwork/skills/source_pkg-my-skill'), + ); + expect(await installed.exists(), isTrue); + }); + + test('when installing then copies SKILL.md as-is', () async { + await adapter.installSkill(skill); + + final skillMd = File( + d.path('project/.qoderwork/skills/source_pkg-my-skill/SKILL.md'), + ); + final content = await skillMd.readAsString(); + + expect(content, contains('name: source_pkg-my-skill')); + expect(content, contains('# My Skill')); + }); + + test('when installing then copies supporting directories', () async { + await adapter.installSkill(skill); + + final script = File( + d.path( + 'project/.qoderwork/skills/source_pkg-my-skill/scripts/run.sh'), + ); + expect(await script.exists(), isTrue); + + final ref = File( + d.path( + 'project/.qoderwork/skills/source_pkg-my-skill/references/guide.md', + ), + ); + expect(await ref.exists(), isTrue); + }); + + test('when reinstalling then replaces existing skill', () async { + await adapter.installSkill(skill); + await adapter.installSkill(skill); + + final installed = Directory( + d.path('project/.qoderwork/skills/source_pkg-my-skill'), + ); + expect(await installed.exists(), isTrue); + }); + }); + + group('and a previously installed skill', () { + setUp(() async { + await d.dir('project', [ + d.dir('.qoderwork', [ + d.dir('skills', [ + d.dir('pkg-old-skill', [ + d.file( + 'SKILL.md', + '---\nname: pkg-old-skill\n' + 'description: old\n---\nOld.', + ), + ]), + ]), + ]), + ]).create(); + }); + + test('when removing then deletes the skill directory', () async { + await adapter.removeSkill('pkg-old-skill'); + + final removed = Directory( + d.path('project/.qoderwork/skills/pkg-old-skill'), + ); + expect(await removed.exists(), isFalse); + }); + }); + }); +}