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:
parent
e58d9bd639
commit
2aff3f405e
|
|
@ -1,5 +1,6 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const yaml = require('yaml');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
|
@ -39,8 +40,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
const conflict = await this.findAncestorConflict(projectDir);
|
const conflict = await this.findAncestorConflict(projectDir);
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
await prompts.log.error(
|
await prompts.log.error(
|
||||||
`Found existing BMAD commands in ancestor installation: ${conflict}\n` +
|
`Found existing BMAD skills in ancestor installation: ${conflict}\n` +
|
||||||
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
|
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` +
|
||||||
` Please remove the BMAD files from that directory first:\n` +
|
` Please remove the BMAD files from that directory first:\n` +
|
||||||
` rm -rf "${conflict}"/bmad*`,
|
` rm -rf "${conflict}"/bmad*`,
|
||||||
);
|
);
|
||||||
|
|
@ -165,8 +166,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, 'agent', extension);
|
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++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,8 +204,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, 'workflow', extension);
|
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++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,8 +252,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, artifact.type, extension);
|
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') {
|
if (artifact.type === 'task') {
|
||||||
taskCount++;
|
taskCount++;
|
||||||
|
|
@ -425,6 +441,71 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
return rendered;
|
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
|
* Generate filename for artifact
|
||||||
* @param {Object} artifact - Artifact data
|
* @param {Object} artifact - Artifact data
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,11 @@ platforms:
|
||||||
category: cli
|
category: cli
|
||||||
description: "Anthropic's official CLI for Claude"
|
description: "Anthropic's official CLI for Claude"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .claude/commands
|
legacy_targets:
|
||||||
|
- .claude/commands
|
||||||
|
target_dir: .claude/skills
|
||||||
template_type: default
|
template_type: default
|
||||||
|
skill_format: true
|
||||||
ancestor_conflict_check: true
|
ancestor_conflict_check: true
|
||||||
|
|
||||||
cline:
|
cline:
|
||||||
|
|
@ -203,9 +206,11 @@ platforms:
|
||||||
# artifact_types: [agents, workflows, tasks, tools]
|
# artifact_types: [agents, workflows, tasks, tools]
|
||||||
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
||||||
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
# 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
|
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
|
||||||
# # in the same target_dir (for IDEs that inherit
|
# # in the same target_dir (for IDEs that inherit
|
||||||
# # commands from parent directories)
|
# # skills from parent directories)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Platform Categories
|
# Platform Categories
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue