Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/skills/migrate-skills-yaml/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ shared_docs:
skills:
- id: nextjs-app-router
type: example
example_path: basics/next-app-router
example_paths: basics/next-app-router
display_name: Next.js App Router
description: PostHog integration for Next.js App Router applications
tags: [nextjs, react, ssr, app-router, javascript]
Expand Down Expand Up @@ -45,7 +45,7 @@ shared_docs:
- https://posthog.com/docs/getting-started/identify-users.md
variants:
- id: nextjs-app-router
example_path: basics/next-app-router
example_paths: basics/next-app-router
display_name: Next.js App Router
tags: [nextjs, react, ssr, app-router, javascript]
docs_urls:
Expand Down Expand Up @@ -101,5 +101,5 @@ variants:
1. Identify which group each skill belongs to (integration, feature-flag, llm-analytics, logs, or other)
2. Add each skill as a variant in the appropriate `transformation-config/skills/{group}-skills.yaml` file
3. Remove `type` from the variant (inherited from group) unless it needs to differ
4. Keep `id`, `display_name`, `description` (optional), `tags`, `docs_urls`, and `example_path` (for example-type skills)
4. Keep `id`, `display_name`, `description` (optional), `tags`, `docs_urls`, and `example_paths` (for example-type skills)
5. Remove the old flat file
1 change: 1 addition & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function generateManifest(skills, uriSchema, version, guideContents = {}) {
name: skill.name,
description: skill.description,
tags: skill.tags,
...(skill.metadata ? { metadata: skill.metadata } : {}),
uri,
};

Expand Down
68 changes: 45 additions & 23 deletions scripts/lib/skill-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ function loadSkillTemplate(configDir, compositeKey, templateFile) {
return fs.readFileSync(filePath, 'utf8');
}

/**
* Normalize example_paths to an array.
* Accepts undefined, a string, or an array of strings.
*/
function normalizeExamplePaths(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}

/**
* Expand grouped skill config into a flat array of skill objects.
* Each top-level key (except shared_docs) is a skill group with
Expand All @@ -93,6 +102,8 @@ function expandSkillGroups(config, configDir) {
const baseType = group.type || 'example';
const baseDescription = group.description || null;
const baseSharedDocs = group.shared_docs || [];
const baseMetadata = group.metadata || {};
const baseExamplePaths = normalizeExamplePaths(group.example_paths);

// Category is the first segment of the composite key, or an explicit override
const category = group.category || key.split('/')[0];
Expand All @@ -119,8 +130,10 @@ function expandSkillGroups(config, configDir) {
// Support per-variation shared_docs (merged with base)
const sharedDocs = [...baseSharedDocs, ...(variation.shared_docs || [])];

// Skill ID: {compositeKey-dashed}-{shortId}
const skillId = `${compositeKeyDashed}-${variation.id}`;
// Skill ID: {compositeKey-dashed}-{shortId}, dropping the "-all" suffix
const skillId = variation.id === 'all'
? compositeKeyDashed
: `${compositeKeyDashed}-${variation.id}`;

skills.push({
...variation,
Expand All @@ -133,7 +146,9 @@ function expandSkillGroups(config, configDir) {
description,
_template: template,
_sharedDocs: sharedDocs,
_examplePaths: [...baseExamplePaths, ...normalizeExamplePaths(variation.example_paths)],
_group: key,
_metadata: { ...baseMetadata, ...(variation.metadata || {}) },
});
}
}
Expand Down Expand Up @@ -398,6 +413,7 @@ function generateFrontmatter(skill, version) {
metadata: {
author: 'PostHog',
version: version,
...(skill._metadata || {}),
},
};

Expand Down Expand Up @@ -441,29 +457,34 @@ async function generateSkill({
// Track reference files for the SKILL.md listing
const references = [];

// Process example code if this is an example-based skill
if (skill.type === 'example' && skill.example_path) {
console.log(` Processing example: ${skill.example_path}`);

const exampleMarkdown = processExample({
examplePath: skill.example_path,
displayName: skill.display_name,
id: skill.id,
repoRoot,
skipPatterns: mergeSkipPatterns(skipPatterns.global, skipPatterns.examples[skill.id]),
plugins: defaultPlugins,
});
// Process example projects
if (skill._examplePaths && skill._examplePaths.length > 0) {
const isSingle = skill._examplePaths.length === 1;
for (const examplePath of skill._examplePaths) {
const dirName = path.basename(examplePath);
console.log(` Processing example: ${examplePath}`);

const exampleMarkdown = processExample({
examplePath,
displayName: isSingle ? skill.display_name : dirName,
id: skill.id,
repoRoot,
skipPatterns: mergeSkipPatterns(skipPatterns.global, skipPatterns.examples[isSingle ? skill.id : dirName]),
plugins: defaultPlugins,
});

fs.writeFileSync(
path.join(referencesDir, 'EXAMPLE.md'),
exampleMarkdown,
'utf8'
);
const filename = isSingle ? 'EXAMPLE.md' : `EXAMPLE-${dirName}.md`;
fs.writeFileSync(
path.join(referencesDir, filename),
exampleMarkdown,
'utf8'
);

references.push({
filename: 'EXAMPLE.md',
description: `${skill.display_name} example project code`,
});
references.push({
filename,
description: `${isSingle ? skill.display_name : dirName} example project code`,
});
}
}

// Helper to process a doc entry (string URL or {url, title} object)
Expand Down Expand Up @@ -625,6 +646,7 @@ async function generateAllSkills({
name: s.description,
description: s.description,
tags: s.tags || [],
metadata: s._metadata && Object.keys(s._metadata).length > 0 ? s._metadata : undefined,
}));
}

Expand Down
177 changes: 177 additions & 0 deletions scripts/lib/tests/skill-group-expander.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,181 @@ describe('expandSkillGroups', () => {
// id still uses composite key, not category
expect(skills[0].id).toBe('feature-flags-installation-react');
});

it('omits variant id from skill id when variant id is "all"', () => {
createFixture({
skills: {
'instrument-product-analytics': {
'description.md': '# Product analytics',
},
},
}, tmpDir);
const config = {
'instrument-product-analytics': {
type: 'docs-only',
template: 'description.md',
variants: [{ id: 'all', display_name: 'all frameworks' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0].id).toBe('instrument-product-analytics');
expect(skills[0]._shortId).toBe('all');
});

it('passes group-level metadata through to _metadata', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
metadata: { consumer: 'agent' },
variants: [{ id: 'all', display_name: 'all frameworks' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._metadata).toEqual({ consumer: 'agent' });
});

it('merges variant-level metadata over group-level metadata', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
metadata: { consumer: 'agent', author: 'base' },
variants: [{ id: 'all', display_name: 'all', metadata: { author: 'override' } }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._metadata).toEqual({ consumer: 'agent', author: 'override' });
});

it('passes group-level example_paths through to _examplePaths', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
example_paths: ['basics/django', 'basics/flask'],
variants: [{ id: 'all', display_name: 'all frameworks' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._examplePaths).toEqual(['basics/django', 'basics/flask']);
});

it('merges variant-level example_paths on top of group-level', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
example_paths: ['basics/django'],
variants: [{ id: 'all', display_name: 'all', example_paths: ['basics/flask'] }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._examplePaths).toEqual(['basics/django', 'basics/flask']);
});

it('normalizes string example_paths to array', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
example_paths: 'basics/django',
variants: [{ id: 'django', display_name: 'Django' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._examplePaths).toEqual(['basics/django']);
});

it('normalizes variant-level string example_paths to array', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
variants: [{ id: 'django', display_name: 'Django', example_paths: 'basics/django' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._examplePaths).toEqual(['basics/django']);
});

it('defaults _examplePaths to empty array when not specified', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
variants: [{ id: 'django', display_name: 'Django' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._examplePaths).toEqual([]);
});

it('defaults _metadata to empty object when not specified', () => {
createFixture({
skills: {
integration: {
'description.md': '# Integration',
},
},
}, tmpDir);
const config = {
integration: {
type: 'docs-only',
template: 'description.md',
variants: [{ id: 'django', display_name: 'Django' }],
},
};
const skills = expandSkillGroups(config, tmpDir);
expect(skills[0]._metadata).toEqual({});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Aggregated feature flag skill - all platforms in one skill
type: docs-only
template: description.md
category: feature-flags
description: Add PostHog feature flags to gate new functionality. Use after implementing features or reviewing PRs to ensure safe rollouts with feature flag controls. Also handles initial PostHog SDK setup if not yet installed.
tags: [feature-flags]
metadata:
consumer: coding-agent
shared_docs:
- https://posthog.com/docs/feature-flags/adding-feature-flag-code.md
- https://posthog.com/docs/feature-flags/best-practices.md
variants:
- id: all
display_name: all supported platforms
tags: []
docs_urls:
- https://posthog.com/docs/feature-flags/installation/react.md
- https://posthog.com/docs/feature-flags/installation/react-native.md
- https://posthog.com/docs/feature-flags/installation/web.md
- https://posthog.com/docs/feature-flags/installation/nodejs.md
- https://posthog.com/docs/feature-flags/installation/python.md
- https://posthog.com/docs/feature-flags/installation/php.md
- https://posthog.com/docs/feature-flags/installation/ruby.md
- https://posthog.com/docs/feature-flags/installation/go.md
- https://posthog.com/docs/feature-flags/installation/java.md
- https://posthog.com/docs/feature-flags/installation/rust.md
- https://posthog.com/docs/feature-flags/installation/dotnet.md
- https://posthog.com/docs/feature-flags/installation/elixir.md
- https://posthog.com/docs/feature-flags/installation/android.md
- https://posthog.com/docs/feature-flags/installation/ios.md
- https://posthog.com/docs/feature-flags/installation/flutter.md
- https://posthog.com/docs/feature-flags/installation/api.md
- https://posthog.com/docs/libraries/next-js.md
Loading
Loading