From cacaf6530a054c2a78c133fe911107f94e3fb965 Mon Sep 17 00:00:00 2001 From: bercianor Date: Wed, 3 Jun 2026 00:30:18 +0200 Subject: [PATCH 1/4] feat(install-skills): add --agent parameter to select the agent for which to install them --- README.md | 31 +++++++ gradle.properties | 2 +- .../command/InstallSkillsCommand.java | 7 +- .../skills/SkillsInstallationTarget.java | 20 ++++ .../SkillsInstallationTargetResolver.java | 37 +++++++- .../command/InstallSkillsCommandTest.java | 92 +++++++++++++++++++ .../SkillsInstallationTargetResolverTest.java | 68 +++++++++++++- .../skills/SkillsInstallationTargetTest.java | 57 ++++++++++++ 8 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java diff --git a/README.md b/README.md index 686050b..674abfd 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Built for **DevOps engineers**, **Platform engineers**, **Administrators**, and | `audit fix` | Fix a change's audit state (`APPLIED` or `ROLLED_BACK`) | | `issue list` | List changes with audit issues | | `issue get` | Get details and resolution guidance for an issue | +| `install-skills` | Install official Flamingock AI skills into the current project | ### Global Options @@ -127,6 +128,36 @@ flamingock audit list --jar ./my-app.jar -J -Xmx1g -- --spring.profiles.active=s --- +## 🧠 Installing AI Skills + +Use `install-skills` to copy the official Flamingock skills into the directory for the AI assistant you use in the current project. + +### Quick path + +```bash +# Default project-local install +flamingock install-skills + +# Claude project-local install +flamingock install-skills --agent claude + +# Install into both project-local locations +flamingock install-skills --agent all +``` + +### Current targets + +| Command | Destination | +|---------|-------------| +| `flamingock install-skills` | `./.agents/skills` | +| `flamingock install-skills --agent claude` | `./.claude/skills` | +| `flamingock install-skills --agent all` | `./.agents/skills` and `./.claude/skills` | + +Note: +- Paths are resolved relative to the directory where you run the command. + +--- + ## 📦 Installation Choose the option that best fits your platform: diff --git a/gradle.properties b/gradle.properties index c6cb592..fa79a43 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=io.flamingock -version=1.2.0 +version=1.2.1 diff --git a/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java b/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java index 33c3fe2..cd1ac70 100644 --- a/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java +++ b/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java @@ -40,6 +40,11 @@ public class InstallSkillsCommand implements Callable { @Option(names = {"-g", "--global"}, description = "Install skills globally (not implemented yet)") private boolean global; + @Option(names = {"-a", "--agent"}, + description = "Target AI assistant: agents, claude, or all (default: agents)", + defaultValue = "agents") + private String agent; + private final SkillsInstallationTargetResolver targetResolver; private final SkillsInstallationPipeline pipeline; private final Path workingDirectory; @@ -69,7 +74,7 @@ public InstallSkillsCommand() { @Override public Integer call() { try { - List targets = targetResolver.resolveTargets(workingDirectory.toAbsolutePath().normalize(), global); + List targets = targetResolver.resolveTargets(workingDirectory.toAbsolutePath().normalize(), global, agent); SkillsInstallationResult result = pipeline.install(targets); ConsoleFormatter.printInfo(buildSuccessMessage(result)); return 0; diff --git a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java index 0fad640..682584c 100644 --- a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java +++ b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java @@ -40,4 +40,24 @@ public record SkillsInstallationTarget(String identifier, Path destinationSkills public static SkillsInstallationTarget local(Path destinationSkillsDir) { return new SkillsInstallationTarget("local", destinationSkillsDir); } + + /** + * Creates an agent-specific installation target for the agents assistant. + * + * @param destinationSkillsDir agent destination directory + * @return agents installation target + */ + public static SkillsInstallationTarget agents(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:agents", destinationSkillsDir); + } + + /** + * Creates an agent-specific installation target for the claude assistant. + * + * @param destinationSkillsDir claude destination directory + * @return claude installation target + */ + public static SkillsInstallationTarget claude(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:claude", destinationSkillsDir); + } } diff --git a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java index 34ec77d..84d7ab2 100644 --- a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java +++ b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java @@ -26,6 +26,7 @@ public class SkillsInstallationTargetResolver { private static final String[] LOCAL_SKILLS_PATH = {".agents", "skills"}; + private static final String[] CLAUDE_PATH = {".claude", "skills"}; private static final String GLOBAL_MODE_NOT_IMPLEMENTED = "Global skills installation is not implemented yet. Run 'flamingock install-skills' to install into ./.agents/skills."; @@ -40,18 +41,48 @@ public SkillsInstallationTargetResolver() { } /** - * Resolves the skills installation targets for the requested mode. + * Resolves the skills installation targets for the requested mode (default agent). * * @param workingDirectory current command working directory * @param global whether global mode was requested * @return resolved installation targets */ public List resolveTargets(Path workingDirectory, boolean global) { + return resolveTargets(workingDirectory, global, null); + } + + /** + * Resolves the skills installation targets for the requested mode and agent. + * + * @param workingDirectory current command working directory + * @param global whether global mode was requested + * @param agent target AI assistant identifier (agents, claude, all; defaults to agents) + * @return resolved installation targets + */ + public List resolveTargets(Path workingDirectory, boolean global, String agent) { if (global) { throw new IllegalStateException(GLOBAL_MODE_NOT_IMPLEMENTED); } - Path destination = directoryResolver.resolveDirectory(workingDirectory, LOCAL_SKILLS_PATH); - return List.of(SkillsInstallationTarget.local(destination)); + return switch (agent != null ? agent : "agents") { + case "agents" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, LOCAL_SKILLS_PATH); + yield List.of(SkillsInstallationTarget.agents(destination)); + } + case "claude" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, CLAUDE_PATH); + yield List.of(SkillsInstallationTarget.claude(destination)); + } + case "all" -> { + Path agentsDest = directoryResolver.resolveDirectory(workingDirectory, LOCAL_SKILLS_PATH); + Path claudeDest = directoryResolver.resolveDirectory(workingDirectory, CLAUDE_PATH); + yield List.of( + SkillsInstallationTarget.agents(agentsDest), + SkillsInstallationTarget.claude(claudeDest) + ); + } + default -> throw new IllegalStateException( + "Unsupported agent: '" + agent + "'. Supported values: claude, all."); + }; } } diff --git a/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java b/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java index 8f48eac..c11e6f3 100644 --- a/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java +++ b/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class InstallSkillsCommandTest { @@ -40,6 +41,85 @@ class InstallSkillsCommandTest { @TempDir Path tempDir; + @Test + void call_defaultAgentPassesAgentsToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute(); + + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("agents", targetResolver.agent); + } + + @Test + void call_withAgentClaudePassesClaudeToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.claude(tempDir.resolve(".claude/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute("-a", "claude"); + + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("claude", targetResolver.agent); + } + + @Test + void call_withAgentAllPassesAllToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute("--agent", "all"); + + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("all", targetResolver.agent); + } + + @Test + void call_withAgentAllPrintsTwoDestinationsInSuccessMessage() throws Exception { + SkillsInstallationTarget agentsTarget = SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills")); + SkillsInstallationTarget claudeTarget = SkillsInstallationTarget.claude(tempDir.resolve(".claude/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(agentsTarget, claudeTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(agentsTarget, claudeTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent, true, StandardCharsets.UTF_8)); + try { + int exitCode = new CommandLine(command).execute("--agent", "all"); + + assertEquals(0, exitCode); + } finally { + System.setOut(originalOut); + } + + assertTrue(targetResolver.called); + assertTrue(pipeline.called); + String output = outContent.toString(StandardCharsets.UTF_8); + assertTrue(output.contains("2 destinations")); + } + @Test void rootCommand_registersInstallSkillsSubcommand() { CommandLine commandLine = new CommandLine(new FlamingockExecutorCli()); @@ -152,6 +232,7 @@ private static final class RecordingTargetResolver extends SkillsInstallationTar private boolean called; private Path workingDirectory; private boolean global; + private String agent; private RecordingTargetResolver(List targets) { this.targets = targets; @@ -159,9 +240,15 @@ private RecordingTargetResolver(List targets) { @Override public List resolveTargets(Path workingDirectory, boolean global) { + return resolveTargets(workingDirectory, global, null); + } + + @Override + public List resolveTargets(Path workingDirectory, boolean global, String agent) { this.called = true; this.workingDirectory = workingDirectory; this.global = global; + this.agent = agent; return targets; } } @@ -178,6 +265,11 @@ private FailingTargetResolver(IllegalStateException failure) { @Override public List resolveTargets(Path workingDirectory, boolean global) { + return resolveTargets(workingDirectory, global, null); + } + + @Override + public List resolveTargets(Path workingDirectory, boolean global, String agent) { this.called = true; this.global = global; throw failure; diff --git a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java index d0d9303..a6eb871 100644 --- a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java +++ b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java @@ -44,7 +44,73 @@ void resolveTargets_localModeResolvesSingleProjectTarget() { assertTrue(directoryResolver.called); assertEquals(tempDir, directoryResolver.workingDirectory); assertArrayEquals(new String[]{".agents", "skills"}, directoryResolver.segments); - assertEquals(List.of(SkillsInstallationTarget.local(tempDir.resolve(".agents/skills"))), targets); + assertEquals(List.of(SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills"))), targets); + } + + @Test + void resolveTargets_agentsAgentResolvesToAgentsPath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".agents/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "agents"); + + assertEquals(1, targets.size()); + assertEquals("local:agents", targets.get(0).identifier()); + assertArrayEquals(new String[]{".agents", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_claudeAgentResolvesToClaudePath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".claude/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "claude"); + + assertEquals(1, targets.size()); + assertEquals("local:claude", targets.get(0).identifier()); + assertArrayEquals(new String[]{".claude", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_allAgentReturnsBothTargets() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".agents/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "all"); + + assertEquals(2, targets.size()); + assertEquals("local:agents", targets.get(0).identifier()); + assertEquals("local:claude", targets.get(1).identifier()); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_invalidAgentThrowsWithSupportedValues() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".agents/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> resolver.resolveTargets(tempDir, false, "cursor")); + + assertFalse(directoryResolver.called); + String message = exception.getMessage(); + assertTrue(message.contains("cursor")); + assertTrue(message.contains("claude")); + assertTrue(message.contains("all")); + } + + @Test + void resolveTargets_globalModeStillThrowsBeforeAgentEval() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".claude/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> resolver.resolveTargets(tempDir, true, "claude")); + + assertFalse(directoryResolver.called); + assertTrue(exception.getMessage().contains("not implemented yet")); } @Test diff --git a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java new file mode 100644 index 0000000..4fc4a82 --- /dev/null +++ b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.cli.executor.skills; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class SkillsInstallationTargetTest { + + @Test + void agentsFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.agents(null), + "destinationSkillsDir must not be null"); + } + + @Test + void claudeFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.claude(null), + "destinationSkillsDir must not be null"); + } + + @Test + void agentsFactoryCreatesTargetWithAgentsIdentifier() { + Path dest = Path.of("/some/path/.agents/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.agents(dest); + + assertEquals("local:agents", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void claudeFactoryCreatesTargetWithClaudeIdentifier() { + Path dest = Path.of("/some/path/.claude/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.claude(dest); + + assertEquals("local:claude", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } +} From deaa87dbe1a29e24f24b12b0a75358b3ae8ae875 Mon Sep 17 00:00:00 2001 From: bercianor Date: Wed, 3 Jun 2026 12:21:35 +0200 Subject: [PATCH 2/4] fix: remove 'agents' option to --agent, default is null --- .../command/InstallSkillsCommand.java | 3 +- .../SkillsInstallationTargetResolver.java | 13 ++++---- .../command/InstallSkillsCommandTest.java | 30 +++++++++++++++++-- .../SkillsInstallationTargetResolverTest.java | 19 ++++++++++-- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java b/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java index cd1ac70..b51bffa 100644 --- a/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java +++ b/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java @@ -41,8 +41,7 @@ public class InstallSkillsCommand implements Callable { private boolean global; @Option(names = {"-a", "--agent"}, - description = "Target AI assistant: agents, claude, or all (default: agents)", - defaultValue = "agents") + description = "Target AI assistant: claude or all (default: agents, local project)") private String agent; private final SkillsInstallationTargetResolver targetResolver; diff --git a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java index 84d7ab2..01366ac 100644 --- a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java +++ b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java @@ -56,7 +56,7 @@ public List resolveTargets(Path workingDirectory, bool * * @param workingDirectory current command working directory * @param global whether global mode was requested - * @param agent target AI assistant identifier (agents, claude, all; defaults to agents) + * @param agent target AI assistant identifier (claude or all; null defaults to agents) * @return resolved installation targets */ public List resolveTargets(Path workingDirectory, boolean global, String agent) { @@ -64,11 +64,12 @@ public List resolveTargets(Path workingDirectory, bool throw new IllegalStateException(GLOBAL_MODE_NOT_IMPLEMENTED); } - return switch (agent != null ? agent : "agents") { - case "agents" -> { - Path destination = directoryResolver.resolveDirectory(workingDirectory, LOCAL_SKILLS_PATH); - yield List.of(SkillsInstallationTarget.agents(destination)); - } + if (agent == null) { + Path destination = directoryResolver.resolveDirectory(workingDirectory, LOCAL_SKILLS_PATH); + return List.of(SkillsInstallationTarget.agents(destination)); + } + + return switch (agent) { case "claude" -> { Path destination = directoryResolver.resolveDirectory(workingDirectory, CLAUDE_PATH); yield List.of(SkillsInstallationTarget.claude(destination)); diff --git a/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java b/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java index c11e6f3..c5817fd 100644 --- a/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java +++ b/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java @@ -42,7 +42,7 @@ class InstallSkillsCommandTest { Path tempDir; @Test - void call_defaultAgentPassesAgentsToResolver() { + void call_defaultAgentPassesNullToResolver() { SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills")); RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( @@ -55,7 +55,33 @@ void call_defaultAgentPassesAgentsToResolver() { assertEquals(0, exitCode); assertTrue(targetResolver.called); - assertEquals("agents", targetResolver.agent); + assertNull(targetResolver.agent); + } + + @Test + void call_withInvalidAgentAgentsReturnsExitCodeOne() { + FailingTargetResolver targetResolver = new FailingTargetResolver( + new IllegalStateException("Unsupported agent: 'agents'. Supported values: claude, all.") + ); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult(List.of(), List.of())); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(errContent, true, StandardCharsets.UTF_8)); + try { + int exitCode = new CommandLine(command).execute("-a", "agents"); + + assertEquals(1, exitCode); + } finally { + System.setErr(originalErr); + } + + assertTrue(targetResolver.called); + String stderr = errContent.toString(StandardCharsets.UTF_8); + assertTrue(stderr.contains("agents")); + assertTrue(stderr.contains("claude")); + assertTrue(stderr.contains("all")); } @Test diff --git a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java index a6eb871..4b698e5 100644 --- a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java +++ b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java @@ -48,11 +48,11 @@ void resolveTargets_localModeResolvesSingleProjectTarget() { } @Test - void resolveTargets_agentsAgentResolvesToAgentsPath() { + void resolveTargets_nullAgentResolvesToAgentsPath() { RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".agents/skills")); SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); - List targets = resolver.resolveTargets(tempDir, false, "agents"); + List targets = resolver.resolveTargets(tempDir, false, (String) null); assertEquals(1, targets.size()); assertEquals("local:agents", targets.get(0).identifier()); @@ -60,6 +60,21 @@ void resolveTargets_agentsAgentResolvesToAgentsPath() { assertTrue(directoryResolver.called); } + @Test + void resolveTargets_agentsAgentThrowsWithSupportedValues() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".agents/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> resolver.resolveTargets(tempDir, false, "agents")); + + assertFalse(directoryResolver.called); + String message = exception.getMessage(); + assertTrue(message.contains("agents")); + assertTrue(message.contains("claude")); + assertTrue(message.contains("all")); + } + @Test void resolveTargets_claudeAgentResolvesToClaudePath() { RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".claude/skills")); From a5836cb6ba5b070494fa865f150acfc6ed3bca25 Mon Sep 17 00:00:00 2001 From: bercianor Date: Wed, 3 Jun 2026 21:42:14 +0200 Subject: [PATCH 3/4] feat: add some more agent paths --- README.md | 48 +++++--- .../command/InstallSkillsCommand.java | 2 +- .../skills/SkillsInstallationTarget.java | 60 ++++++++++ .../SkillsInstallationTargetResolver.java | 40 +++++-- .../command/InstallSkillsCommandTest.java | 108 ++++++++++++++---- .../SkillsInstallationTargetResolverTest.java | 96 ++++++++++++++-- .../skills/SkillsInstallationTargetTest.java | 96 ++++++++++++++++ 7 files changed, 393 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 674abfd..f1b46f9 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ Built for **DevOps engineers**, **Platform engineers**, **Administrators**, and ### Operations -| Command | Description | -|-----------------|---------------------------------------------------------| -| `execute apply` | Apply pending changes | -| `audit list` | List audit entries (snapshot or full history) | -| `audit fix` | Fix a change's audit state (`APPLIED` or `ROLLED_BACK`) | -| `issue list` | List changes with audit issues | -| `issue get` | Get details and resolution guidance for an issue | +| Command | Description | +|------------------|----------------------------------------------------------------| +| `execute apply` | Apply pending changes | +| `audit list` | List audit entries (snapshot or full history) | +| `audit fix` | Fix a change's audit state (`APPLIED` or `ROLLED_BACK`) | +| `issue list` | List changes with audit issues | +| `issue get` | Get details and resolution guidance for an issue | | `install-skills` | Install official Flamingock AI skills into the current project | ### Global Options @@ -141,17 +141,37 @@ flamingock install-skills # Claude project-local install flamingock install-skills --agent claude -# Install into both project-local locations -flamingock install-skills --agent all +# GitHub Copilot project-local install +flamingock install-skills --agent github + +# Cursor project-local install +flamingock install-skills --agent cursor + +# OpenCode project-local install +flamingock install-skills --agent opencode + +# Google Gemini project-local install +flamingock install-skills --agent gemini + +# Windsurf project-local install +flamingock install-skills --agent windsurf + +# PI Agent project-local install +flamingock install-skills --agent pi ``` ### Current targets -| Command | Destination | -|---------|-------------| -| `flamingock install-skills` | `./.agents/skills` | -| `flamingock install-skills --agent claude` | `./.claude/skills` | -| `flamingock install-skills --agent all` | `./.agents/skills` and `./.claude/skills` | +| Command | Destination | +|----------------------------------------------|----------------------| +| `flamingock install-skills` | `./.agents/skills` | +| `flamingock install-skills --agent claude` | `./.claude/skills` | +| `flamingock install-skills --agent github` | `./.github/skills` | +| `flamingock install-skills --agent cursor` | `./.cursor/skills` | +| `flamingock install-skills --agent opencode` | `./.opencode/skills` | +| `flamingock install-skills --agent gemini` | `./.gemini/skills` | +| `flamingock install-skills --agent windsurf` | `./.windsurf/skills` | +| `flamingock install-skills --agent pi` | `./.pi/skills` | Note: - Paths are resolved relative to the directory where you run the command. diff --git a/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java b/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java index b51bffa..79e57f7 100644 --- a/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java +++ b/src/main/java/io/flamingock/cli/executor/command/InstallSkillsCommand.java @@ -41,7 +41,7 @@ public class InstallSkillsCommand implements Callable { private boolean global; @Option(names = {"-a", "--agent"}, - description = "Target AI assistant: claude or all (default: agents, local project)") + description = "Target AI assistant: claude, github, cursor, opencode, gemini, windsurf, pi") private String agent; private final SkillsInstallationTargetResolver targetResolver; diff --git a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java index 682584c..7cca7c5 100644 --- a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java +++ b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTarget.java @@ -60,4 +60,64 @@ public static SkillsInstallationTarget agents(Path destinationSkillsDir) { public static SkillsInstallationTarget claude(Path destinationSkillsDir) { return new SkillsInstallationTarget("local:claude", destinationSkillsDir); } + + /** + * Creates an agent-specific installation target for the github assistant. + * + * @param destinationSkillsDir github destination directory + * @return github installation target + */ + public static SkillsInstallationTarget github(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:github", destinationSkillsDir); + } + + /** + * Creates an agent-specific installation target for the cursor assistant. + * + * @param destinationSkillsDir cursor destination directory + * @return cursor installation target + */ + public static SkillsInstallationTarget cursor(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:cursor", destinationSkillsDir); + } + + /** + * Creates an agent-specific installation target for the opencode assistant. + * + * @param destinationSkillsDir opencode destination directory + * @return opencode installation target + */ + public static SkillsInstallationTarget opencode(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:opencode", destinationSkillsDir); + } + + /** + * Creates an agent-specific installation target for the gemini assistant. + * + * @param destinationSkillsDir gemini destination directory + * @return gemini installation target + */ + public static SkillsInstallationTarget gemini(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:gemini", destinationSkillsDir); + } + + /** + * Creates an agent-specific installation target for the windsurf assistant. + * + * @param destinationSkillsDir windsurf destination directory + * @return windsurf installation target + */ + public static SkillsInstallationTarget windsurf(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:windsurf", destinationSkillsDir); + } + + /** + * Creates an agent-specific installation target for the pi agent assistant. + * + * @param destinationSkillsDir pi agent destination directory + * @return pi agent installation target + */ + public static SkillsInstallationTarget pi(Path destinationSkillsDir) { + return new SkillsInstallationTarget("local:pi", destinationSkillsDir); + } } diff --git a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java index 01366ac..c01bdf3 100644 --- a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java +++ b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolver.java @@ -27,6 +27,12 @@ public class SkillsInstallationTargetResolver { private static final String[] LOCAL_SKILLS_PATH = {".agents", "skills"}; private static final String[] CLAUDE_PATH = {".claude", "skills"}; + private static final String[] GITHUB_PATH = {".github", "skills"}; + private static final String[] CURSOR_PATH = {".cursor", "skills"}; + private static final String[] OPENCODE_PATH = {".opencode", "skills"}; + private static final String[] GEMINI_PATH = {".gemini", "skills"}; + private static final String[] WINDSURF_PATH = {".windsurf", "skills"}; + private static final String[] PI_PATH = {".pi", "skills"}; private static final String GLOBAL_MODE_NOT_IMPLEMENTED = "Global skills installation is not implemented yet. Run 'flamingock install-skills' to install into ./.agents/skills."; @@ -56,7 +62,7 @@ public List resolveTargets(Path workingDirectory, bool * * @param workingDirectory current command working directory * @param global whether global mode was requested - * @param agent target AI assistant identifier (claude or all; null defaults to agents) + * @param agent target AI assistant identifier (claude, github, cursor, opencode, gemini, windsurf, pi) * @return resolved installation targets */ public List resolveTargets(Path workingDirectory, boolean global, String agent) { @@ -74,16 +80,32 @@ public List resolveTargets(Path workingDirectory, bool Path destination = directoryResolver.resolveDirectory(workingDirectory, CLAUDE_PATH); yield List.of(SkillsInstallationTarget.claude(destination)); } - case "all" -> { - Path agentsDest = directoryResolver.resolveDirectory(workingDirectory, LOCAL_SKILLS_PATH); - Path claudeDest = directoryResolver.resolveDirectory(workingDirectory, CLAUDE_PATH); - yield List.of( - SkillsInstallationTarget.agents(agentsDest), - SkillsInstallationTarget.claude(claudeDest) - ); + case "github" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, GITHUB_PATH); + yield List.of(SkillsInstallationTarget.github(destination)); + } + case "cursor" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, CURSOR_PATH); + yield List.of(SkillsInstallationTarget.cursor(destination)); + } + case "opencode" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, OPENCODE_PATH); + yield List.of(SkillsInstallationTarget.opencode(destination)); + } + case "gemini" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, GEMINI_PATH); + yield List.of(SkillsInstallationTarget.gemini(destination)); + } + case "windsurf" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, WINDSURF_PATH); + yield List.of(SkillsInstallationTarget.windsurf(destination)); + } + case "pi" -> { + Path destination = directoryResolver.resolveDirectory(workingDirectory, PI_PATH); + yield List.of(SkillsInstallationTarget.pi(destination)); } default -> throw new IllegalStateException( - "Unsupported agent: '" + agent + "'. Supported values: claude, all."); + "Unsupported agent: '" + agent + "'. Supported values: claude, github, cursor, opencode, gemini, windsurf, pi."); }; } } diff --git a/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java b/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java index c5817fd..a088a29 100644 --- a/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java +++ b/src/test/java/io/flamingock/cli/executor/command/InstallSkillsCommandTest.java @@ -61,7 +61,7 @@ void call_defaultAgentPassesNullToResolver() { @Test void call_withInvalidAgentAgentsReturnsExitCodeOne() { FailingTargetResolver targetResolver = new FailingTargetResolver( - new IllegalStateException("Unsupported agent: 'agents'. Supported values: claude, all.") + new IllegalStateException("Unsupported agent: 'agents'. Supported values: claude, github, cursor, opencode, gemini, windsurf, pi.") ); RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult(List.of(), List.of())); InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); @@ -81,7 +81,12 @@ void call_withInvalidAgentAgentsReturnsExitCodeOne() { String stderr = errContent.toString(StandardCharsets.UTF_8); assertTrue(stderr.contains("agents")); assertTrue(stderr.contains("claude")); - assertTrue(stderr.contains("all")); + assertTrue(stderr.contains("github")); + assertTrue(stderr.contains("cursor")); + assertTrue(stderr.contains("opencode")); + assertTrue(stderr.contains("gemini")); + assertTrue(stderr.contains("windsurf")); + assertTrue(stderr.contains("pi")); } @Test @@ -102,8 +107,8 @@ void call_withAgentClaudePassesClaudeToResolver() { } @Test - void call_withAgentAllPassesAllToResolver() { - SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills")); + void call_withAgentGithubPassesGithubToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.github(tempDir.resolve(".github/skills")); RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( List.of(resolvedTarget), @@ -111,39 +116,96 @@ void call_withAgentAllPassesAllToResolver() { )); InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); - int exitCode = new CommandLine(command).execute("--agent", "all"); + int exitCode = new CommandLine(command).execute("-a", "github"); assertEquals(0, exitCode); assertTrue(targetResolver.called); - assertEquals("all", targetResolver.agent); + assertEquals("github", targetResolver.agent); } @Test - void call_withAgentAllPrintsTwoDestinationsInSuccessMessage() throws Exception { - SkillsInstallationTarget agentsTarget = SkillsInstallationTarget.agents(tempDir.resolve(".agents/skills")); - SkillsInstallationTarget claudeTarget = SkillsInstallationTarget.claude(tempDir.resolve(".claude/skills")); - RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(agentsTarget, claudeTarget)); + void call_withAgentCursorPassesCursorToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.cursor(tempDir.resolve(".cursor/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( - List.of(agentsTarget, claudeTarget), + List.of(resolvedTarget), List.of("flamingock-core") )); InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); - ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - PrintStream originalOut = System.out; - System.setOut(new PrintStream(outContent, true, StandardCharsets.UTF_8)); - try { - int exitCode = new CommandLine(command).execute("--agent", "all"); + int exitCode = new CommandLine(command).execute("-a", "cursor"); - assertEquals(0, exitCode); - } finally { - System.setOut(originalOut); - } + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("cursor", targetResolver.agent); + } + + @Test + void call_withAgentOpencodePassesOpencodeToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.opencode(tempDir.resolve(".opencode/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute("-a", "opencode"); + assertEquals(0, exitCode); assertTrue(targetResolver.called); - assertTrue(pipeline.called); - String output = outContent.toString(StandardCharsets.UTF_8); - assertTrue(output.contains("2 destinations")); + assertEquals("opencode", targetResolver.agent); + } + + @Test + void call_withAgentGeminiPassesGeminiToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.gemini(tempDir.resolve(".gemini/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute("-a", "gemini"); + + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("gemini", targetResolver.agent); + } + + @Test + void call_withAgentWindsurfPassesWindsurfToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.windsurf(tempDir.resolve(".windsurf/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute("-a", "windsurf"); + + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("windsurf", targetResolver.agent); + } + + @Test + void call_withAgentPiPassesPiToResolver() { + SkillsInstallationTarget resolvedTarget = SkillsInstallationTarget.pi(tempDir.resolve(".pi/skills")); + RecordingTargetResolver targetResolver = new RecordingTargetResolver(List.of(resolvedTarget)); + RecordingPipeline pipeline = new RecordingPipeline(new SkillsInstallationResult( + List.of(resolvedTarget), + List.of("flamingock-core") + )); + InstallSkillsCommand command = new InstallSkillsCommand(targetResolver, pipeline, tempDir); + + int exitCode = new CommandLine(command).execute("-a", "pi"); + + assertEquals(0, exitCode); + assertTrue(targetResolver.called); + assertEquals("pi", targetResolver.agent); } @Test diff --git a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java index 4b698e5..704f629 100644 --- a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java +++ b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetResolverTest.java @@ -72,7 +72,12 @@ void resolveTargets_agentsAgentThrowsWithSupportedValues() { String message = exception.getMessage(); assertTrue(message.contains("agents")); assertTrue(message.contains("claude")); - assertTrue(message.contains("all")); + assertTrue(message.contains("github")); + assertTrue(message.contains("cursor")); + assertTrue(message.contains("opencode")); + assertTrue(message.contains("gemini")); + assertTrue(message.contains("windsurf")); + assertTrue(message.contains("pi")); } @Test @@ -89,15 +94,81 @@ void resolveTargets_claudeAgentResolvesToClaudePath() { } @Test - void resolveTargets_allAgentReturnsBothTargets() { - RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".agents/skills")); + void resolveTargets_geminiAgentResolvesToGeminiPath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".gemini/skills")); SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); - List targets = resolver.resolveTargets(tempDir, false, "all"); + List targets = resolver.resolveTargets(tempDir, false, "gemini"); - assertEquals(2, targets.size()); - assertEquals("local:agents", targets.get(0).identifier()); - assertEquals("local:claude", targets.get(1).identifier()); + assertEquals(1, targets.size()); + assertEquals("local:gemini", targets.get(0).identifier()); + assertEquals(tempDir.resolve(".gemini/skills"), targets.get(0).destinationSkillsDir()); + assertArrayEquals(new String[]{".gemini", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_githubAgentResolvesToGithubPath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".github/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "github"); + + assertEquals(1, targets.size()); + assertEquals("local:github", targets.get(0).identifier()); + assertArrayEquals(new String[]{".github", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_cursorAgentResolvesToCursorPath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".cursor/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "cursor"); + + assertEquals(1, targets.size()); + assertEquals("local:cursor", targets.get(0).identifier()); + assertArrayEquals(new String[]{".cursor", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_opencodeAgentResolvesToOpencodePath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".opencode/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "opencode"); + + assertEquals(1, targets.size()); + assertEquals("local:opencode", targets.get(0).identifier()); + assertArrayEquals(new String[]{".opencode", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_windsurfAgentResolvesToWindsurfPath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".windsurf/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "windsurf"); + + assertEquals(1, targets.size()); + assertEquals("local:windsurf", targets.get(0).identifier()); + assertArrayEquals(new String[]{".windsurf", "skills"}, directoryResolver.segments); + assertTrue(directoryResolver.called); + } + + @Test + void resolveTargets_piAgentResolvesToPiPath() { + RecordingDirectoryResolver directoryResolver = new RecordingDirectoryResolver(tempDir.resolve(".pi/skills")); + SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); + + List targets = resolver.resolveTargets(tempDir, false, "pi"); + + assertEquals(1, targets.size()); + assertEquals("local:pi", targets.get(0).identifier()); + assertArrayEquals(new String[]{".pi", "skills"}, directoryResolver.segments); assertTrue(directoryResolver.called); } @@ -107,13 +178,18 @@ void resolveTargets_invalidAgentThrowsWithSupportedValues() { SkillsInstallationTargetResolver resolver = new SkillsInstallationTargetResolver(directoryResolver); IllegalStateException exception = assertThrows(IllegalStateException.class, - () -> resolver.resolveTargets(tempDir, false, "cursor")); + () -> resolver.resolveTargets(tempDir, false, "foo")); assertFalse(directoryResolver.called); String message = exception.getMessage(); - assertTrue(message.contains("cursor")); + assertTrue(message.contains("foo")); assertTrue(message.contains("claude")); - assertTrue(message.contains("all")); + assertTrue(message.contains("github")); + assertTrue(message.contains("cursor")); + assertTrue(message.contains("opencode")); + assertTrue(message.contains("gemini")); + assertTrue(message.contains("windsurf")); + assertTrue(message.contains("pi")); } @Test diff --git a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java index 4fc4a82..1d9f885 100644 --- a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java +++ b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationTargetTest.java @@ -54,4 +54,100 @@ void claudeFactoryCreatesTargetWithClaudeIdentifier() { assertEquals("local:claude", target.identifier()); assertEquals(dest, target.destinationSkillsDir()); } + + @Test + void githubFactoryCreatesTargetWithGithubIdentifier() { + Path dest = Path.of("/some/path/.github/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.github(dest); + + assertEquals("local:github", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void githubFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.github(null), + "destinationSkillsDir must not be null"); + } + + @Test + void cursorFactoryCreatesTargetWithCursorIdentifier() { + Path dest = Path.of("/some/path/.cursor/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.cursor(dest); + + assertEquals("local:cursor", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void cursorFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.cursor(null), + "destinationSkillsDir must not be null"); + } + + @Test + void opencodeFactoryCreatesTargetWithOpencodeIdentifier() { + Path dest = Path.of("/some/path/.opencode/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.opencode(dest); + + assertEquals("local:opencode", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void opencodeFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.opencode(null), + "destinationSkillsDir must not be null"); + } + + @Test + void geminiFactoryCreatesTargetWithGeminiIdentifier() { + Path dest = Path.of("/some/path/.gemini/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.gemini(dest); + + assertEquals("local:gemini", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void geminiFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.gemini(null), + "destinationSkillsDir must not be null"); + } + + @Test + void windsurfFactoryCreatesTargetWithWindsurfIdentifier() { + Path dest = Path.of("/some/path/.windsurf/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.windsurf(dest); + + assertEquals("local:windsurf", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void windsurfFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.windsurf(null), + "destinationSkillsDir must not be null"); + } + + @Test + void piFactoryCreatesTargetWithPiIdentifier() { + Path dest = Path.of("/some/path/.pi/skills"); + SkillsInstallationTarget target = SkillsInstallationTarget.pi(dest); + + assertEquals("local:pi", target.identifier()); + assertEquals(dest, target.destinationSkillsDir()); + } + + @Test + void piFactoryRejectsNullPath() { + assertThrows(NullPointerException.class, + () -> SkillsInstallationTarget.pi(null), + "destinationSkillsDir must not be null"); + } } From 3cf3bcaa8492b096ce3f06464905f0f5cc6f9b3c Mon Sep 17 00:00:00 2001 From: bercianor Date: Thu, 11 Jun 2026 13:16:12 +0200 Subject: [PATCH 4/4] feat: remove evals directory inside skill before move to final location --- .../skills/SkillsInstallationPipeline.java | 3 ++ .../SkillsInstallationPipelineTest.java | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationPipeline.java b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationPipeline.java index ff8cd66..ca7b548 100644 --- a/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationPipeline.java +++ b/src/main/java/io/flamingock/cli/executor/skills/SkillsInstallationPipeline.java @@ -110,6 +110,9 @@ public SkillsInstallationResult install(List targets) snapshotRoot, path -> path.getFileName().toString().startsWith(SKILL_DIRECTORY_PREFIX) ); + for (Path skillDirectory : skillDirectories) { + FileSystemUtils.deleteRecursively(skillDirectory.resolve("evals")); + } List installedSkills = new ArrayList<>(); for (Path skillDirectory : skillDirectories) { installedSkills.add(skillDirectory.getFileName().toString()); diff --git a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationPipelineTest.java b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationPipelineTest.java index f8a30f6..5cfb398 100644 --- a/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationPipelineTest.java +++ b/src/test/java/io/flamingock/cli/executor/skills/SkillsInstallationPipelineTest.java @@ -205,6 +205,53 @@ public List listDirectories(Path root, java.util.function.Predicate assertEquals("custom", Files.readString(destination.resolve("my-custom-skill").resolve("SKILL.md"))); } + @Test + void install_removesEvalsFromExtractedSkillsBeforeCopy() throws Exception { + Path destination = Files.createDirectories(tempDir.resolve(".agents").resolve("skills")); + SkillsInstallationTarget target = SkillsInstallationTarget.local(destination); + + Path snapshotRoot = Files.createDirectories(tempDir.resolve("snapshot")); + Path skillWithEvals = Files.createDirectories(snapshotRoot.resolve("flamingock-core")); + Files.writeString(skillWithEvals.resolve("SKILL.md"), "main"); + Path evalsDir = Files.createDirectories(skillWithEvals.resolve("evals")); + Files.writeString(evalsDir.resolve("test.java"), "// eval"); + + Path skillWithoutEvals = Files.createDirectories(snapshotRoot.resolve("flamingock-java")); + Files.writeString(skillWithoutEvals.resolve("SKILL.md"), "main"); + + SkillsInstallationPipeline pipeline = new SkillsInstallationPipeline( + new HttpFileDownloader() { + @Override + public Path downloadTo(Path workspace, URI sourceUri, String targetFileName, String userAgent, String downloadLabel) { + return tempDir.resolve("downloaded.zip"); + } + }, + new ZipArchiveExtractor() { + @Override + public Path extractSingleRootDirectory(Path archive, Path workspace, String extractionDirectoryName, + String archiveDescription) { + return snapshotRoot; + } + }, + new DirectoryLister() { + @Override + public List listDirectories(Path root, java.util.function.Predicate filter) { + return List.of(skillWithEvals, skillWithoutEvals); + } + }, + new DirectoryReplacer(), + () -> tempDir.resolve("workspace"), + FileSystemUtils::deleteRecursively + ); + + SkillsInstallationResult result = pipeline.install(List.of(target)); + + assertEquals(List.of("flamingock-core", "flamingock-java"), result.installedSkills()); + assertFalse(Files.exists(destination.resolve("flamingock-core").resolve("evals"))); + assertEquals("main", Files.readString(destination.resolve("flamingock-core").resolve("SKILL.md"))); + assertEquals("main", Files.readString(destination.resolve("flamingock-java").resolve("SKILL.md"))); + } + @Test void install_downloadsAndExtractsOnceForMultipleTargets() throws Exception { List events = new ArrayList<>();