From abbfd504ac6dd8fe3aa2bb3acfab7f72f24b81c6 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:56:47 +0200 Subject: [PATCH] feat: add agents skills to cli init (#8948) Co-authored-by: Claude Opus 4.5 --- cli/README.md | 1 + cli/src/commands/init/init.ts | 6 ++- cli/src/guidance/writer.ts | 43 ++++++++++------- cli/test/guidance_writer_unit.test.ts | 68 +++++++++++++++++++-------- 4 files changed, 79 insertions(+), 39 deletions(-) diff --git a/cli/README.md b/cli/README.md index 478aa2a936..224951b234 100644 --- a/cli/README.md +++ b/cli/README.md @@ -130,6 +130,7 @@ project guidance shape: - `AGENTS.md` - `CLAUDE.md` +- `.agents/skills/*` - `.claude/skills/*` ### Testing with a local `windmill-yaml-validator` diff --git a/cli/src/commands/init/init.ts b/cli/src/commands/init/init.ts index 519f572394..828da425a0 100644 --- a/cli/src/commands/init/init.ts +++ b/cli/src/commands/init/init.ts @@ -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) { diff --git a/cli/src/guidance/writer.ts b/cli/src/guidance/writer.ts index b301272882..1d1949934f 100644 --- a/cli/src/guidance/writer.ts +++ b/cli/src/guidance/writer.ts @@ -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 { - 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 { - 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 { - const skillsDir = join(targetDir, ".claude", "skills"); - await mkdir(skillsDir, { recursive: true }); - return skillsDir; +async function ensureSkillsDirectories(targetDir: string): Promise { + 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 { diff --git a/cli/test/guidance_writer_unit.test.ts b/cli/test/guidance_writer_unit.test.ts index dbe541e65a..4399639905 100644 --- a/cli/test/guidance_writer_unit.test.ts +++ b/cli/test/guidance_writer_unit.test.ts @@ -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): Promise { 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 + ); + } }); });