mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
feat: add agents skills to cli init (#8948)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
15bba79ef2
commit
abbfd504ac
@@ -130,6 +130,7 @@ project guidance shape:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `CLAUDE.md`
|
||||
- `.agents/skills/*`
|
||||
- `.claude/skills/*`
|
||||
|
||||
### Testing with a local `windmill-yaml-validator`
|
||||
|
||||
@@ -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
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user