feat(skills): claude-code installer outputs .claude/skills/<name>/SKILL.md

Refactor the config-driven installer to emit Agent Skills Open Standard
format for Claude Code: directory-per-skill with SKILL.md entrypoint,
unquoted YAML frontmatter, and full canonical names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-03-06 02:03:43 -07:00
parent e58d9bd639
commit 2aff3f405e
2 changed files with 96 additions and 10 deletions

View File

@ -1,5 +1,6 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
@ -39,8 +40,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const conflict = await this.findAncestorConflict(projectDir);
if (conflict) {
await prompts.log.error(
`Found existing BMAD commands in ancestor installation: ${conflict}\n` +
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
`Found existing BMAD skills in ancestor installation: ${conflict}\n` +
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` +
` Please remove the BMAD files from that directory first:\n` +
` rm -rf "${conflict}"/bmad*`,
);
@ -165,8 +166,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
for (const artifact of artifacts) {
const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'agent', extension);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
}
count++;
}
@ -198,8 +204,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'workflow', extension);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
}
count++;
}
}
@ -241,8 +252,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, artifact.type, extension);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
}
if (artifact.type === 'task') {
taskCount++;
@ -425,6 +441,71 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
return rendered;
}
/**
* Write artifact as a skill directory with SKILL.md inside.
* Mirrors codex.js writeSkillArtifacts() approach.
* @param {string} targetPath - Base skills directory
* @param {Object} artifact - Artifact data
* @param {string} content - Rendered template content
*/
async writeSkillFile(targetPath, artifact, content) {
const { resolveSkillName } = require('./shared/path-utils');
// Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md
const flatName = resolveSkillName(artifact);
const skillName = path.basename(flatName.replace(/\.md$/, ''));
if (!skillName) {
throw new Error(`Cannot derive skill name for artifact: ${artifact.relativePath || JSON.stringify(artifact)}`);
}
// Create skill directory
const skillDir = path.join(targetPath, skillName);
await fs.ensureDir(skillDir);
// Transform content: rewrite frontmatter for skills format
const skillContent = this.transformToSkillFormat(content, skillName);
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf8');
}
/**
* Transform artifact content to Agent Skills format.
* Rewrites frontmatter to contain only unquoted name and description.
* @param {string} content - Original content with YAML frontmatter
* @param {string} skillName - Skill name (must match directory name)
* @returns {string} Transformed content
*/
transformToSkillFormat(content, skillName) {
// Normalize line endings
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
// Parse frontmatter
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!fmMatch) {
// No frontmatter -- wrap with minimal frontmatter
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
return `---\n${fm}\n---\n\n${content}`;
}
const frontmatter = fmMatch[1];
const body = fmMatch[2];
// Parse frontmatter with yaml library to extract description
let description;
try {
const parsed = yaml.parse(frontmatter);
const rawDesc = parsed?.description;
description = typeof rawDesc === 'string' && rawDesc ? rawDesc : `${skillName} skill`;
} catch {
description = `${skillName} skill`;
}
// Build new frontmatter with only name and description, unquoted
const newFrontmatter = yaml.stringify({ name: skillName, description: String(description) }, { lineWidth: 0 }).trimEnd();
return `---\n${newFrontmatter}\n---\n${body}`;
}
/**
* Generate filename for artifact
* @param {Object} artifact - Artifact data

View File

@ -38,8 +38,11 @@ platforms:
category: cli
description: "Anthropic's official CLI for Claude"
installer:
target_dir: .claude/commands
legacy_targets:
- .claude/commands
target_dir: .claude/skills
template_type: default
skill_format: true
ancestor_conflict_check: true
cline:
@ -203,9 +206,11 @@ platforms:
# artifact_types: [agents, workflows, tasks, tools]
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
# skill_format: boolean (optional) # Use directory-per-skill output: <name>/SKILL.md
# # with clean frontmatter (name + description, unquoted)
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
# # in the same target_dir (for IDEs that inherit
# # commands from parent directories)
# # skills from parent directories)
# ============================================================================
# Platform Categories