diff --git a/context/skills/integration/config.yaml b/context/skills/integration/config.yaml index 1dae71c4..d676a1fd 100644 --- a/context/skills/integration/config.yaml +++ b/context/skills/integration/config.yaml @@ -1,4 +1,5 @@ # Integration skills - example-based (code + docs) +# `framework` = the detection id a variant serves; `default: true` marks the family default. Both are emitted into skill-menu.json. type: skill template: description.md description: PostHog integration for {display_name} applications @@ -6,6 +7,8 @@ shared_docs: - https://posthog.com/docs/getting-started/identify-users.md variants: - id: nextjs-app-router + framework: nextjs + default: true example_paths: example-apps/next-app-router display_name: Next.js App Router tags: [nextjs, react, ssr, app-router, javascript, javascript_web, javascript_node] @@ -13,6 +16,7 @@ variants: - https://posthog.com/docs/libraries/next-js.md - id: nextjs-pages-router + framework: nextjs example_paths: example-apps/next-pages-router display_name: Next.js Pages Router tags: [nextjs, react, ssr, pages-router, javascript, javascript_web, javascript_node] @@ -20,6 +24,8 @@ variants: - https://posthog.com/docs/libraries/next-js.md - id: react-react-router-6 + framework: react-router + default: true example_paths: example-apps/react-react-router-6 display_name: React Router v6 tags: [react, react-router, v6, spa, javascript, javascript_web] @@ -27,6 +33,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v6.md - id: react-react-router-7-framework + framework: react-router example_paths: example-apps/react-react-router-7-framework display_name: React Router v7 - Framework mode tags: [react, react-router, v7, framework, ssr, javascript, javascript_node, javascript_web] @@ -34,6 +41,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v7-framework-mode.md - id: react-react-router-7-data + framework: react-router example_paths: example-apps/react-react-router-7-data display_name: React Router v7 - Data mode tags: [react, react-router, v7, data, spa, javascript, javascript_web] @@ -41,6 +49,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v7-data-mode.md - id: react-react-router-7-declarative + framework: react-router example_paths: example-apps/react-react-router-7-declarative display_name: React Router v7 - Declarative mode tags: [react, react-router, v7, declarative, spa, javascript, javascript_web] @@ -56,6 +65,8 @@ variants: - https://posthog.com/docs/libraries/react.md - id: nuxt-3-6 + framework: nuxt + default: true example_paths: example-apps/nuxt-3-6 display_name: Nuxt 3.6 description: PostHog integration for Nuxt versions 3.0 to 3.6 @@ -64,6 +75,7 @@ variants: - https://posthog.com/docs/libraries/nuxt-js-3-6.md - id: nuxt-4 + framework: nuxt example_paths: example-apps/nuxt-4 display_name: Nuxt 4 description: PostHog integration for Nuxt 4 applications @@ -72,6 +84,7 @@ variants: - https://posthog.com/docs/libraries/nuxt-js.md - id: vue-3 + framework: vue example_paths: example-apps/vue-3 display_name: Vue 3 description: PostHog integration for Vue 3 applications @@ -80,6 +93,7 @@ variants: - https://posthog.com/docs/libraries/vue-js.md - id: django + framework: django example_paths: example-apps/django display_name: Django tags: [django, python] @@ -87,6 +101,7 @@ variants: - https://posthog.com/docs/libraries/django.md - id: flask + framework: flask example_paths: example-apps/flask display_name: Flask tags: [flask, python] @@ -94,6 +109,7 @@ variants: - https://posthog.com/docs/libraries/flask.md - id: fastapi + framework: fastapi example_paths: example-apps/fastapi display_name: FastAPI description: PostHog integration for FastAPI applications @@ -102,6 +118,7 @@ variants: - https://posthog.com/docs/libraries/python.md - id: react-tanstack-router-file-based + framework: tanstack-router example_paths: example-apps/react-tanstack-router-file-based display_name: React with TanStack Router (file-based) description: PostHog integration for React applications using TanStack Router with file-based routing @@ -110,6 +127,8 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: react-tanstack-router-code-based + framework: tanstack-router + default: true example_paths: example-apps/react-tanstack-router-code-based display_name: React with TanStack Router (code-based) description: PostHog integration for React applications using TanStack Router with code-based routing @@ -118,6 +137,7 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: tanstack-start + framework: tanstack-start example_paths: example-apps/tanstack-start display_name: TanStack Start description: PostHog integration for TanStack Start full-stack applications @@ -126,6 +146,7 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: laravel + framework: laravel example_paths: example-apps/laravel display_name: Laravel tags: [laravel, php] @@ -141,6 +162,7 @@ variants: - https://posthog.com/docs/libraries/php.md - id: ruby-on-rails + framework: rails example_paths: example-apps/ruby-on-rails display_name: Ruby on Rails tags: [ruby-on-rails, ruby] @@ -149,6 +171,7 @@ variants: - https://posthog.com/docs/libraries/ruby.md - id: android + framework: android example_paths: example-apps/android display_name: Android description: PostHog integration for Android applications @@ -157,6 +180,7 @@ variants: - https://posthog.com/docs/libraries/android.md - id: sveltekit + framework: sveltekit example_paths: example-apps/sveltekit display_name: SvelteKit description: PostHog integration for SvelteKit applications @@ -166,6 +190,7 @@ variants: # Language fallback skills, used when no specific framework is detected - id: python + framework: python example_paths: example-apps/python display_name: Python description: PostHog integration for any Python application using the Python SDK @@ -175,6 +200,7 @@ variants: - https://posthog.com/docs/references/posthog-python.md - id: javascript_node + framework: javascript_node example_path: example-apps/javascript-node display_name: JavaScript Node description: PostHog integration for server-side Node.js applications using posthog-node @@ -184,6 +210,7 @@ variants: - https://posthog.com/docs/references/posthog-node.md - id: javascript_web + framework: javascript_web example_path: example-apps/javascript-web display_name: JavaScript Web description: PostHog integration for client-side web JavaScript applications using posthog-js @@ -193,6 +220,7 @@ variants: - https://posthog.com/docs/references/posthog-js.md - id: ruby + framework: ruby example_paths: example-apps/ruby display_name: Ruby description: PostHog integration for any Ruby application using the Ruby SDK @@ -217,6 +245,7 @@ variants: - https://posthog.com/docs/libraries/go.md - id: swift + framework: swift example_paths: - example-apps/swift - example-apps/swift-xcodegen @@ -237,6 +266,8 @@ variants: - https://posthog.com/docs/libraries/flutter.md - id: react-native + framework: react-native + default: true example_paths: example-apps/react-native display_name: React Native description: PostHog integration for React Native applications @@ -245,6 +276,7 @@ variants: - https://posthog.com/docs/libraries/react-native.md - id: expo + framework: react-native example_paths: example-apps/expo display_name: Expo description: PostHog integration for Expo applications @@ -253,6 +285,7 @@ variants: - https://posthog.com/docs/libraries/react-native.md - id: astro-static + framework: astro example_paths: example-apps/astro-static display_name: Astro (Static) description: PostHog integration for static Astro sites using SSG @@ -261,6 +294,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-view-transitions + framework: astro example_paths: example-apps/astro-view-transitions display_name: Astro (View Transitions) description: PostHog integration for Astro with ClientRouter view transitions @@ -269,6 +303,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-ssr + framework: astro example_paths: example-apps/astro-ssr display_name: Astro (SSR) description: PostHog integration for server-rendered Astro applications with API routes @@ -277,6 +312,8 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-hybrid + framework: astro + default: true example_paths: example-apps/astro-hybrid display_name: Astro (Hybrid) description: PostHog integration for Astro hybrid rendering with both static and server-rendered pages @@ -285,6 +322,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: angular + framework: angular example_paths: example-apps/angular display_name: Angular description: PostHog integration for Angular applications diff --git a/scripts/lib/agent-generator.js b/scripts/lib/agent-generator.js index 10689301..93e8aac3 100644 --- a/scripts/lib/agent-generator.js +++ b/scripts/lib/agent-generator.js @@ -46,14 +46,19 @@ export function loadAgentEntries(agentsSourceDir) { } /** - * A prompt's frontmatter `flow:` must match its folder — the folder is the - * registry scope, the frontmatter keeps the file self-describing on disk. + * A prompt's frontmatter `flow:` must be present and match its folder — + * consumers filter by it, so a missing key would silently drop the prompt. */ function assertFlowMatches(sourcePath, flow) { const text = fs.readFileSync(sourcePath, 'utf8'); const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); const declared = match?.[1].match(/^flow:\s*(.+?)\s*$/m)?.[1]; - if (declared && declared !== flow) { + if (!declared) { + throw new Error( + `Agent prompt ${sourcePath} is missing the "flow:" frontmatter key — declare flow: ${flow}`, + ); + } + if (declared !== flow) { throw new Error( `Agent prompt ${sourcePath} declares flow "${declared}" but lives in agents/${flow}/`, ); diff --git a/scripts/lib/build-phases.js b/scripts/lib/build-phases.js index a52e08d8..d65370de 100644 --- a/scripts/lib/build-phases.js +++ b/scripts/lib/build-phases.js @@ -190,11 +190,16 @@ function writeManifestAndMenu({ allSkills, docContents, distDir, configDir, vers for (const skill of allSkills) { const cat = skill.group; if (!skillsByCategory[cat]) skillsByCategory[cat] = []; - skillsByCategory[cat].push({ + // group/framework/default let consumers resolve a bare skill id + framework by exact match. + const entry = { id: skill.id, name: skill.name, + group: skill.group.replace(/\//g, '-'), downloadUrl: manifest.resources.find(r => r.id === skill.id)?.downloadUrl, - }); + }; + if (skill.framework) entry.framework = skill.framework; + if (skill.default) entry.default = true; + skillsByCategory[cat].push(entry); } // The CLI entries are the lookup table the wizard's runtime resolver uses diff --git a/scripts/lib/skill-generator.js b/scripts/lib/skill-generator.js index e67998b6..86b8b825 100644 --- a/scripts/lib/skill-generator.js +++ b/scripts/lib/skill-generator.js @@ -228,15 +228,19 @@ function normalizeExamplePaths(value) { /** * Resolve `variants_from` references: a group may borrow another group's * variant matrix instead of duplicating it. Only the framework identity comes - * across — id, display_name, tags, docs_urls — never example paths, templates, - * cli blocks, or shared docs, which stay the borrowing group's own concern. - * One level only; a source group must declare its variants literally. + * across — id, display_name, tags, docs_urls, framework, default — never + * example paths, templates, cli blocks, or shared docs, which stay the + * borrowing group's own concern. One level only; a source group must declare + * its variants literally. Pure — returns a new config, never mutates the input. */ function resolveVariantsFrom(config) { + const resolved = {}; for (const [key, group] of Object.entries(config)) { - if (!group.variants_from) continue; + if (!group.variants_from) { + resolved[key] = group; + continue; + } if (group.variants) { - if (group._variantsResolved) continue; throw new Error(`Skill group "${key}": declare either variants or variants_from, not both`); } const source = config[group.variants_from]; @@ -246,14 +250,19 @@ function resolveVariantsFrom(config) { if (source.variants_from) { throw new Error(`Skill group "${key}": variants_from cannot chain ("${group.variants_from}" also uses variants_from)`); } - group.variants = source.variants.map(v => { + const variants = source.variants.map(v => { const variant = { id: v.id, display_name: v.display_name }; if (v.tags) variant.tags = [...v.tags]; if (v.docs_urls) variant.docs_urls = [...v.docs_urls]; + if (v.framework) variant.framework = v.framework; + if (v.default) variant.default = v.default; return variant; }); - group._variantsResolved = true; + // Drop the reference key — the resolved copy declares variants literally. + const { variants_from: _from, ...rest } = group; + resolved[key] = { ...rest, variants }; } + return resolved; } /** @@ -263,9 +272,9 @@ function resolveVariantsFrom(config) { */ function expandSkillGroups(config, configDir) { const skills = []; - resolveVariantsFrom(config); + const resolvedConfig = resolveVariantsFrom(config); - for (const [key, group] of Object.entries(config)) { + for (const [key, group] of Object.entries(resolvedConfig)) { if (key === 'shared_docs') continue; if (!group.variants) continue; @@ -796,6 +805,12 @@ function serializeSkill(s) { description: s.description, tags: s.tags || [], }; + if (s.framework) { + result.framework = s.framework; + } + if (s.default) { + result.default = true; + } if (s._cli) { result.cli = s._cli; } diff --git a/scripts/lib/tests/agent-generator-flow.test.js b/scripts/lib/tests/agent-generator-flow.test.js new file mode 100644 index 00000000..8e4e54d1 --- /dev/null +++ b/scripts/lib/tests/agent-generator-flow.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +import { buildAgents } from '../agent-generator.js'; + +const prompt = (frontmatter) => `---\n${frontmatter}\n---\n\n## Goal\n\nDo the thing.\n`; + +describe('buildAgents flow frontmatter', () => { + let tmpDir; + let configDir; + let distDir; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'agents-test-')); + configDir = join(tmpDir, 'context'); + distDir = join(tmpDir, 'dist'); + mkdirSync(join(configDir, 'agents', 'my-flow'), { recursive: true }); + }); + + afterEach(() => rmSync(tmpDir, { recursive: true, force: true })); + + it('builds a prompt whose flow matches its folder', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + const { count } = buildAgents({ configDir, distDir, baseUrl: 'http://x' }); + expect(count).toBe(1); + const menu = JSON.parse(readFileSync(join(distDir, 'agents', 'agent-menu.json'), 'utf8')); + expect(menu.agents).toEqual([ + { id: 'task', flow: 'my-flow', downloadUrl: 'http://x/my-flow/task.md' }, + ]); + }); + + it('rejects a prompt missing the flow key — consumers filter by it', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task'), + ); + expect(() => buildAgents({ configDir, distDir, baseUrl: 'http://x' })).toThrow( + /missing the "flow:" frontmatter key/, + ); + }); + + it('rejects a prompt whose flow contradicts its folder', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: other-flow'), + ); + expect(() => buildAgents({ configDir, distDir, baseUrl: 'http://x' })).toThrow( + /declares flow "other-flow"/, + ); + }); + + it('still ignores README.md files at both levels', () => { + writeFileSync(join(configDir, 'agents', 'README.md'), '# docs'); + writeFileSync(join(configDir, 'agents', 'my-flow', 'README.md'), '# docs'); + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + const { count } = buildAgents({ configDir, distDir, baseUrl: 'http://x' }); + expect(count).toBe(1); + expect(existsSync(join(distDir, 'agents', 'my-flow', 'README.md'))).toBe(false); + }); +}); diff --git a/scripts/lib/tests/skill-variants-from.test.js b/scripts/lib/tests/skill-variants-from.test.js index f3bcbd79..acf5d833 100644 --- a/scripts/lib/tests/skill-variants-from.test.js +++ b/scripts/lib/tests/skill-variants-from.test.js @@ -40,6 +40,8 @@ describe('variants_from', () => { { id: 'django', display_name: 'Django', + framework: 'django', + default: true, tags: ['django', 'python'], docs_urls: ['https://posthog.com/docs/libraries/django.md'], example_paths: 'example-apps/django', @@ -77,6 +79,21 @@ describe('variants_from', () => { expect(step._examplePaths).toEqual([]); }); + it('borrows framework and default so variant steps resolve like the source', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const step = skills.find(s => s.id === 'flow-x-install-django'); + expect(step.framework).toBe('django'); + expect(step.default).toBe(true); + }); + + it('does not mutate the input config', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + expandSkillGroups(config, tmpDir); + expect(config['flow-x/install'].variants).toBeUndefined(); + expect(config['flow-x/install']._variantsResolved).toBeUndefined(); + }); + it('leaves the source group untouched', () => { const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; const skills = expandSkillGroups(config, tmpDir);