Skip to content

Commit 0a7ce82

Browse files
committed
feat: Add dochia init-skills to generate AI agensts skills based on agentskills.io standard
1 parent 4546c46 commit 0a7ce82

44 files changed

Lines changed: 1071 additions & 35 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
AGENTS.md
2+
.agents
13
*.tape
24
*.iml
35
.idea

src/main/java/dev/dochia/cli/core/command/DochiaCommand.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
InfoCommand.class,
6464
RandomCommand.class,
6565
ExplainCommand.class,
66-
LegendCommand.class
66+
LegendCommand.class,
67+
InitSkillsCommand.class
6768
})
6869
@Unremovable
6970
@Singleton
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package dev.dochia.cli.core.command;
2+
3+
import dev.dochia.cli.core.util.VersionProvider;
4+
import io.github.ludovicianul.prettylogger.PrettyLogger;
5+
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
6+
import io.quarkus.arc.Unremovable;
7+
import picocli.CommandLine;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.nio.charset.StandardCharsets;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
/**
18+
* Generates Agent Skills files for agentic IDE integration.
19+
* Skills are placed in .agents/skills/ following the open Agent Skills specification
20+
* (https://agentskills.io), which is supported by Windsurf, Cursor, Claude Code, and OpenAI Codex.
21+
*/
22+
@CommandLine.Command(
23+
name = "init-skills",
24+
mixinStandardHelpOptions = true,
25+
usageHelpAutoWidth = true,
26+
exitCodeListHeading = "%n@|bold,underline Exit Codes:|@%n",
27+
exitCodeList = {"@|bold 0|@:Successful program execution",
28+
"@|bold 2|@:Usage error: user input for the command was incorrect",
29+
"@|bold 1|@:Internal execution error: an exception occurred when executing command"},
30+
footerHeading = "%n@|bold,underline Examples:|@%n",
31+
footer = {" Initialize agent skills in the current directory:",
32+
" dochia init-skills",
33+
"", " Initialize in a specific directory:",
34+
" dochia init-skills --dir /path/to/project",
35+
"", " Force overwrite existing files:",
36+
" dochia init-skills --force"},
37+
description = "Generate Agent Skills for agentic IDE integration (Windsurf, Cursor, Claude Code, Codex).",
38+
versionProvider = VersionProvider.class)
39+
@Unremovable
40+
public class InitSkillsCommand implements Runnable, CommandLine.IExitCodeGenerator {
41+
private final PrettyLogger logger = PrettyLoggerFactory.getConsoleLogger();
42+
43+
@CommandLine.Option(names = {"--dir", "-d"},
44+
description = "Target directory to generate skills in. Defaults to current directory.",
45+
defaultValue = ".")
46+
private String directory;
47+
48+
@CommandLine.Option(names = {"--force", "-f"},
49+
description = "Overwrite existing files if they already exist.")
50+
private boolean force;
51+
52+
private int exitCode;
53+
54+
private static final List<String> SKILL_NAMES = List.of(
55+
"dochia-test",
56+
"dochia-fuzz",
57+
"dochia-replay",
58+
"dochia-list",
59+
"dochia-explain"
60+
);
61+
62+
private static final Map<String, List<String>> SKILL_EXTRA_FILES = Map.of(
63+
"dochia-test", List.of("references/report-output.md")
64+
);
65+
66+
@Override
67+
public void run() {
68+
try {
69+
Path baseDir = Path.of(directory).toAbsolutePath().normalize();
70+
logger.info("Generating Dochia agent skills in {}", baseDir);
71+
72+
int created = 0;
73+
int skipped = 0;
74+
75+
for (String skillName : SKILL_NAMES) {
76+
Path skillDir = baseDir.resolve(".agents").resolve("skills").resolve(skillName);
77+
Path skillFile = skillDir.resolve("SKILL.md");
78+
String resourcePath = "skills/" + skillName + "/SKILL.md";
79+
80+
if (writeResourceFile(resourcePath, skillFile)) {
81+
created++;
82+
} else {
83+
skipped++;
84+
}
85+
86+
for (String extraFile : SKILL_EXTRA_FILES.getOrDefault(skillName, List.of())) {
87+
Path extraTarget = skillDir.resolve(extraFile);
88+
String extraResource = "skills/" + skillName + "/" + extraFile;
89+
if (writeResourceFile(extraResource, extraTarget)) {
90+
created++;
91+
} else {
92+
skipped++;
93+
}
94+
}
95+
}
96+
97+
logger.noFormat("");
98+
logger.info("Done! {} files created, {} skipped (already exist).", created, skipped);
99+
100+
if (created > 0) {
101+
logger.noFormat("");
102+
logger.info("Generated files:");
103+
for (String skillName : SKILL_NAMES) {
104+
Path skillFile = baseDir.resolve(".agents").resolve("skills").resolve(skillName).resolve("SKILL.md");
105+
if (Files.exists(skillFile)) {
106+
logger.noFormat(" .agents/skills/{}/SKILL.md", skillName);
107+
}
108+
}
109+
logger.noFormat("");
110+
logger.info("These files are automatically discovered by:");
111+
logger.noFormat(" - Windsurf (Cascade)");
112+
logger.noFormat(" - Cursor");
113+
logger.noFormat(" - Claude Code");
114+
logger.noFormat(" - OpenAI Codex");
115+
logger.noFormat("");
116+
logger.info("Commit them to your repository so your team benefits too.");
117+
}
118+
119+
if (skipped > 0 && !force) {
120+
logger.note("Use --force to overwrite existing files.");
121+
}
122+
123+
exitCode = 0;
124+
} catch (Exception e) {
125+
logger.error("Failed to generate agent skills: {}", e.getMessage());
126+
logger.debug("Stacktrace", e);
127+
exitCode = 1;
128+
}
129+
}
130+
131+
/**
132+
* Writes a classpath resource to a target file path.
133+
*
134+
* @return true if the file was written, false if it was skipped
135+
*/
136+
boolean writeResourceFile(String resourcePath, Path targetFile) throws IOException {
137+
if (Files.exists(targetFile) && !force) {
138+
logger.skip(" Skipping {} (already exists)", targetFile.toAbsolutePath().normalize());
139+
return false;
140+
}
141+
142+
String content = readResource(resourcePath);
143+
if (content == null) {
144+
logger.error(" Resource not found: {}", resourcePath);
145+
return false;
146+
}
147+
148+
Files.createDirectories(targetFile.getParent());
149+
Files.writeString(targetFile, content, StandardCharsets.UTF_8);
150+
logger.noFormat(" Created {}", targetFile.toAbsolutePath().normalize());
151+
return true;
152+
}
153+
154+
String readResource(String resourcePath) {
155+
try (InputStream in = Thread.currentThread()
156+
.getContextClassLoader()
157+
.getResourceAsStream(resourcePath)) {
158+
if (in == null) {
159+
return null;
160+
}
161+
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
162+
} catch (IOException e) {
163+
logger.debug("Error reading resource: {}", resourcePath, e);
164+
return null;
165+
}
166+
}
167+
168+
@Override
169+
public int getExitCode() {
170+
return exitCode;
171+
}
172+
}

src/main/java/dev/dochia/cli/core/playbook/body/BypassAuthenticationPlaybook.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,6 @@ public String toString() {
8282

8383
@Override
8484
public String description() {
85-
return " Check if authentication headers are supplied and attempt requests without them";
85+
return "Check if authentication headers are supplied and attempt requests without them";
8686
}
8787
}

src/main/java/dev/dochia/cli/core/playbook/field/NewFieldsPlaybook.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,6 @@ public String toString() {
8888

8989
@Override
9090
public String description() {
91-
return " Send a happy path request and add a new field 'dochiaFuzzyField'";
91+
return "Send a happy path request and add a new field 'dochiaFuzzyField'";
9292
}
9393
}

src/main/java/dev/dochia/cli/core/playbook/special/RandomPlaybook.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ static Map<String, Object> parseYamlAsSimpleMap(String yaml) throws IOException
208208

209209
@Override
210210
public String description() {
211-
return "continuously fuzz random fields with random values based on registered mutators";
211+
return "Continuously fuzz random fields with random values based on registered mutators";
212212
}
213213

214214
@Override

src/main/java/dev/dochia/cli/core/playbook/special/mutators/impl/BigListOfNaughtyStringsMutator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ public String mutate(String inputJson, String selectedField) {
4747

4848
@Override
4949
public String description() {
50-
return "replace field with random naughty strings";
50+
return "Replace field with random naughty strings";
5151
}
5252
}

src/main/java/dev/dochia/cli/core/playbook/special/mutators/impl/LowercaseExpandingBytesMutator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public String mutate(String inputJson, String selectedField) {
1717

1818
@Override
1919
public String description() {
20-
return "replace field with strings that expand bytes when lowercased";
20+
return "Replace field with strings that expand bytes when lowercased";
2121
}
2222
}

src/main/java/dev/dochia/cli/core/playbook/special/mutators/impl/LowercaseExpandingLengthMutator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public String mutate(String inputJson, String selectedField) {
1717

1818
@Override
1919
public String description() {
20-
return "replace field with strings that expand length when lowercased";
20+
return "Replace field with strings that expand length when lowercased";
2121
}
2222
}

src/main/java/dev/dochia/cli/core/playbook/special/mutators/impl/NullStringMutator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public String mutate(String inputJson, String selectedField) {
1717

1818
@Override
1919
public String description() {
20-
return "replace field with null";
20+
return "Replace field with null";
2121
}
2222
}

0 commit comments

Comments
 (0)