feat: add agents skills to cli init (#8948)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-04-27 12:56:47 +00:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 15bba79ef2
commit abbfd504ac
4 changed files with 79 additions and 39 deletions
+1
View File
@@ -130,6 +130,7 @@ project guidance shape:
- `AGENTS.md`
- `CLAUDE.md`
- `.agents/skills/*`
- `.claude/skills/*`
### Testing with a local `windmill-yaml-validator`
+4 -2
View File
@@ -244,7 +244,7 @@ async function initAction(opts: InitOptions) {
// If config can't be read, use default
}
// Create guidance files (AGENTS.md, CLAUDE.md, and Claude skills)
// Create guidance files (AGENTS.md, CLAUDE.md, and agent skills)
try {
const guidanceResult = await writeAiGuidanceFiles({
targetDir: ".",
@@ -262,7 +262,9 @@ async function initAction(opts: InitOptions) {
log.info(colors.green("Created CLAUDE.md"));
}
log.info(
colors.green(`Created .claude/skills/ with ${guidanceResult.skillCount} skills`)
colors.green(
`Created .claude/skills/ and .agents/skills/ with ${guidanceResult.skillCount} skills`
)
);
} catch (error) {
if (error instanceof Error) {
+26 -17
View File
@@ -34,6 +34,7 @@ export const WMILL_INIT_AI_AGENTS_SOURCE_ENV = "WMILL_INIT_AI_AGENTS_SOURCE";
export const WMILL_INIT_AI_CLAUDE_SOURCE_ENV = "WMILL_INIT_AI_CLAUDE_SOURCE";
const CLAUDE_MD_DEFAULT = "Instructions are in @AGENTS.md\n";
const SKILL_TARGET_ROOTS = [".claude", ".agents"] as const;
export async function writeAiGuidanceFiles(
options: WriteAiGuidanceOptions
@@ -86,27 +87,31 @@ async function copySkillsFromSource(
targetDir: string,
skillsSourcePath: string
): Promise<ResolvedSkillMetadata[]> {
const skillsDir = await ensureSkillsDirectory(targetDir);
await copyDirectoryContents(skillsSourcePath, skillsDir);
return await readSkillMetadataFromDirectory(skillsDir);
const skillsDirs = await ensureSkillsDirectories(targetDir);
await Promise.all(
skillsDirs.map((skillsDir) => copyDirectoryContents(skillsSourcePath, skillsDir))
);
return await readSkillMetadataFromDirectory(skillsDirs[0]);
}
async function writeGeneratedSkills(
targetDir: string,
nonDottedPaths: boolean
): Promise<ResolvedSkillMetadata[]> {
const skillsDir = await ensureSkillsDirectory(targetDir);
const skillsDirs = await ensureSkillsDirectories(targetDir);
await Promise.all(
SKILLS.map(async (skill) => {
const skillDir = join(skillsDir, skill.name);
await mkdir(skillDir, { recursive: true });
await writeFile(
join(skillDir, "SKILL.md"),
renderGeneratedSkillContent(skill.name, nonDottedPaths),
"utf8"
);
})
skillsDirs.flatMap((skillsDir) =>
SKILLS.map(async (skill) => {
const skillDir = join(skillsDir, skill.name);
await mkdir(skillDir, { recursive: true });
await writeFile(
join(skillDir, "SKILL.md"),
renderGeneratedSkillContent(skill.name, nonDottedPaths),
"utf8"
);
})
)
);
return SKILLS.map((skill) => ({
@@ -122,10 +127,14 @@ function getGeneratedSkillMetadata(): ResolvedSkillMetadata[] {
}));
}
async function ensureSkillsDirectory(targetDir: string): Promise<string> {
const skillsDir = join(targetDir, ".claude", "skills");
await mkdir(skillsDir, { recursive: true });
return skillsDir;
async function ensureSkillsDirectories(targetDir: string): Promise<string[]> {
const skillsDirs = SKILL_TARGET_ROOTS.map((root) =>
join(targetDir, root, "skills")
);
await Promise.all(
skillsDirs.map((skillsDir) => mkdir(skillsDir, { recursive: true }))
);
return skillsDirs;
}
async function copyDirectoryContents(sourceDir: string, targetDir: string): Promise<void> {
+48 -20
View File
@@ -4,6 +4,8 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { writeAiGuidanceFiles } from "../src/guidance/writer.ts";
const SKILL_TARGET_ROOTS = [".claude", ".agents"] as const;
async function withTempDir(fn: (tempDir: string) => Promise<void>): Promise<void> {
const tempDir = await mkdtemp(join(tmpdir(), "wmill_guidance_writer_"));
try {
@@ -27,7 +29,9 @@ async function writeSkill(
describe("writeAiGuidanceFiles", () => {
test("preserves custom skills when refreshing generated guidance", async () => {
await withTempDir(async (tempDir) => {
const skillsDir = join(tempDir, ".claude", "skills");
const skillsDirs = SKILL_TARGET_ROOTS.map((root) =>
join(tempDir, root, "skills")
);
const customSkillContent = `---
name: custom-skill
description: Custom skill
@@ -37,25 +41,39 @@ Preserve me.
`;
const staleGeneratedContent = "stale generated skill";
const customSkillPath = await writeSkill(skillsDir, "custom-skill", customSkillContent);
const generatedSkillPath = await writeSkill(skillsDir, "write-flow", staleGeneratedContent);
const customSkillPaths = await Promise.all(
skillsDirs.map((skillsDir) =>
writeSkill(skillsDir, "custom-skill", customSkillContent)
)
);
const generatedSkillPaths = await Promise.all(
skillsDirs.map((skillsDir) =>
writeSkill(skillsDir, "write-flow", staleGeneratedContent)
)
);
await writeAiGuidanceFiles({
targetDir: tempDir,
overwriteProjectGuidance: false,
});
expect(await readFile(customSkillPath, "utf8")).toBe(customSkillContent);
for (const customSkillPath of customSkillPaths) {
expect(await readFile(customSkillPath, "utf8")).toBe(customSkillContent);
}
const generatedSkillContent = await readFile(generatedSkillPath, "utf8");
expect(generatedSkillContent).not.toBe(staleGeneratedContent);
expect(generatedSkillContent).toContain("name: write-flow");
for (const generatedSkillPath of generatedSkillPaths) {
const generatedSkillContent = await readFile(generatedSkillPath, "utf8");
expect(generatedSkillContent).not.toBe(staleGeneratedContent);
expect(generatedSkillContent).toContain("name: write-flow");
}
});
});
test("preserves custom skills when copying a skill bundle from source", async () => {
await withTempDir(async (tempDir) => {
const skillsDir = join(tempDir, ".claude", "skills");
const skillsDirs = SKILL_TARGET_ROOTS.map((root) =>
join(tempDir, root, "skills")
);
const customSkillContent = `---
name: custom-skill
description: Custom skill
@@ -78,16 +96,20 @@ description: Bundle only skill
Copied from source bundle.
`;
const customSkillPath = await writeSkill(skillsDir, "custom-skill", customSkillContent);
const existingGeneratedSkillPath = await writeSkill(skillsDir, "write-flow", "old content");
const customSkillPaths = await Promise.all(
skillsDirs.map((skillsDir) =>
writeSkill(skillsDir, "custom-skill", customSkillContent)
)
);
const existingGeneratedSkillPaths = await Promise.all(
skillsDirs.map((skillsDir) =>
writeSkill(skillsDir, "write-flow", "old content")
)
);
const sourceSkillsDir = join(tempDir, "source-skills");
await writeSkill(sourceSkillsDir, "write-flow", sourceSkillContent);
const bundleOnlySkillPath = await writeSkill(
sourceSkillsDir,
"bundle-only",
bundleOnlySkillContent
);
await writeSkill(sourceSkillsDir, "bundle-only", bundleOnlySkillContent);
await writeAiGuidanceFiles({
targetDir: tempDir,
@@ -95,11 +117,17 @@ Copied from source bundle.
skillsSourcePath: sourceSkillsDir,
});
expect(await readFile(customSkillPath, "utf8")).toBe(customSkillContent);
expect(await readFile(existingGeneratedSkillPath, "utf8")).toBe(sourceSkillContent);
expect(await readFile(bundleOnlySkillPath.replace(sourceSkillsDir, skillsDir), "utf8")).toBe(
bundleOnlySkillContent
);
for (const customSkillPath of customSkillPaths) {
expect(await readFile(customSkillPath, "utf8")).toBe(customSkillContent);
}
for (const existingGeneratedSkillPath of existingGeneratedSkillPaths) {
expect(await readFile(existingGeneratedSkillPath, "utf8")).toBe(sourceSkillContent);
}
for (const skillsDir of skillsDirs) {
expect(await readFile(join(skillsDir, "bundle-only", "SKILL.md"), "utf8")).toBe(
bundleOnlySkillContent
);
}
});
});