Skip to content

Commit 005e36a

Browse files
Skills (#330)
* Draft skills * Generate skills * Changeset
1 parent 764c800 commit 005e36a

File tree

137 files changed

+14309
-44
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+14309
-44
lines changed

.changeset/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@repo/elements",
1313
"@repo/examples",
1414
"@repo/typescript-config",
15-
"@repo/shadcn-ui"
15+
"@repo/shadcn-ui",
16+
"@repo/scripts"
1617
]
1718
}

.changeset/wicked-bushes-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ai-elements": patch
3+
---
4+
5+
Add skills
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Generate Skills
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'apps/docs/content/docs/components/**/*.mdx'
9+
- 'packages/examples/src/**/*.tsx'
10+
- 'packages/scripts/**'
11+
12+
concurrency: ${{ github.workflow }}-${{ github.ref }}
13+
14+
permissions:
15+
contents: write
16+
17+
jobs:
18+
generate:
19+
name: Generate Skills
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 5
22+
23+
steps:
24+
- name: Generate GitHub App Token
25+
id: generate-token
26+
uses: actions/create-github-app-token@v2
27+
with:
28+
app-id: ${{ secrets.AI_ELEMENTS_APP_ID }}
29+
private-key: ${{ secrets.AI_ELEMENTS_APP_PRIVATE_KEY }}
30+
31+
- name: Checkout Repo
32+
uses: actions/checkout@v6
33+
with:
34+
token: ${{ steps.generate-token.outputs.token }}
35+
36+
- name: Setup Git User
37+
run: |
38+
git config --global user.email "ai-elements-bot[bot]@users.noreply.github.com"
39+
git config --global user.name "ai-elements-bot[bot]"
40+
41+
- name: Setup pnpm
42+
uses: pnpm/action-setup@v4
43+
44+
- uses: actions/setup-node@v6
45+
with:
46+
node-version: '24'
47+
48+
- name: Install Dependencies
49+
run: pnpm i
50+
51+
- name: Generate Skills
52+
run: pnpm --filter @repo/scripts generate-skills
53+
54+
- name: Commit if changed
55+
run: |
56+
git add -A
57+
if ! git diff --staged --quiet; then
58+
git commit -m "chore: regenerate skills"
59+
git push
60+
fi

packages/scripts/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@repo/scripts",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"generate-skills": "tsx src/generate-skills.ts"
8+
},
9+
"dependencies": {
10+
"gray-matter": "^4.0.3"
11+
},
12+
"devDependencies": {
13+
"@repo/typescript-config": "workspace:*",
14+
"@types/node": "^22.0.0",
15+
"tsx": "^4.19.0",
16+
"typescript": "5.9.3"
17+
}
18+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { existsSync, mkdirSync, rmSync } from 'node:fs';
2+
import { copyFile, readFile, readdir, writeFile } from 'node:fs/promises';
3+
import { basename, join } from 'node:path';
4+
import matter from 'gray-matter';
5+
6+
const ROOT_DIR = join(import.meta.dirname, '../../..');
7+
const DOCS_DIR = join(ROOT_DIR, 'apps/docs/content/docs/components');
8+
const EXAMPLES_DIR = join(ROOT_DIR, 'packages/examples/src');
9+
const SKILLS_DIR = join(ROOT_DIR, 'skills');
10+
11+
const discoverMdxFiles = async (dir: string): Promise<string[]> => {
12+
const results: string[] = [];
13+
const entries = await readdir(dir, { withFileTypes: true });
14+
15+
for (const entry of entries) {
16+
const fullPath = join(dir, entry.name);
17+
if (entry.isDirectory()) {
18+
results.push(...await discoverMdxFiles(fullPath));
19+
} else if (entry.name.endsWith('.mdx')) {
20+
results.push(fullPath);
21+
}
22+
}
23+
24+
return results;
25+
};
26+
27+
const removePreviews = (content: string): string => {
28+
return content.replace(/<Preview\s+path=["'][^"']+["']\s*\/>/g, '');
29+
};
30+
31+
const replaceInstaller = (content: string): string => {
32+
return content.replace(
33+
/<ElementsInstaller\s+path=["']([^"']+)["']\s*\/>/g,
34+
(_, component) => '```bash\nnpx ai-elements@latest add ' + component + '\n```'
35+
);
36+
};
37+
38+
const parseTypeTableProps = (typeContent: string): Array<{
39+
name: string;
40+
type: string;
41+
description: string;
42+
required?: boolean;
43+
default?: string;
44+
}> => {
45+
const props: Array<{
46+
name: string;
47+
type: string;
48+
description: string;
49+
required?: boolean;
50+
default?: string;
51+
}> = [];
52+
53+
const propRegex = /['"]?([^'":\s]+)['"]?\s*:\s*\{([^}]+)\}/g;
54+
let match;
55+
56+
while ((match = propRegex.exec(typeContent)) !== null) {
57+
const propName = match[1];
58+
const propBody = match[2];
59+
60+
const descMatch = propBody.match(/description:\s*['"]([^'"]+)['"]/);
61+
const typeMatch = propBody.match(/type:\s*['"]([^'"]+)['"]/);
62+
const defaultMatch = propBody.match(/default:\s*['"]([^'"]+)['"]/);
63+
const requiredMatch = propBody.match(/required:\s*true/);
64+
65+
props.push({
66+
name: propName,
67+
type: typeMatch?.[1] || 'unknown',
68+
description: descMatch?.[1] || '',
69+
required: !!requiredMatch,
70+
default: defaultMatch?.[1],
71+
});
72+
}
73+
74+
return props;
75+
};
76+
77+
const replaceTypeTables = (content: string): string => {
78+
const typeTableRegex = /<TypeTable\s+type=\{\{([\s\S]*?)\}\}\s*\/>/g;
79+
80+
return content.replace(typeTableRegex, (_, typeContent) => {
81+
const props = parseTypeTableProps(typeContent);
82+
83+
if (props.length === 0) {
84+
return '';
85+
}
86+
87+
const rows = props.map((prop) => {
88+
const name = `\`${prop.name}\``;
89+
const type = `\`${prop.type}\``;
90+
const defaultVal = prop.required
91+
? 'Required'
92+
: prop.default
93+
? `\`${prop.default}\``
94+
: '-';
95+
return `| ${name} | ${type} | ${defaultVal} | ${prop.description} |`;
96+
});
97+
98+
return [
99+
'| Prop | Type | Default | Description |',
100+
'|------|------|---------|-------------|',
101+
...rows,
102+
].join('\n');
103+
});
104+
};
105+
106+
const transformMdx = (fileContent: string, title: string, description: string): string => {
107+
const { content } = matter(fileContent);
108+
109+
let processedContent = removePreviews(content);
110+
processedContent = replaceInstaller(processedContent);
111+
processedContent = replaceTypeTables(processedContent);
112+
113+
const frontmatter = [
114+
'---',
115+
`name: Using the ${title} component from AI Elements`,
116+
`description: ${description}`,
117+
'---',
118+
].join('\n');
119+
120+
return `${frontmatter}\n${processedContent}`;
121+
};
122+
123+
const findMatchingExamples = async (componentName: string): Promise<string[]> => {
124+
const files = await readdir(EXAMPLES_DIR);
125+
126+
return files.filter((file) => {
127+
const fileBasename = file.replace('.tsx', '');
128+
return (
129+
file.endsWith('.tsx') &&
130+
(fileBasename === componentName || fileBasename.startsWith(`${componentName}-`))
131+
);
132+
});
133+
};
134+
135+
const cleanSkillsDir = (): void => {
136+
if (existsSync(SKILLS_DIR)) {
137+
rmSync(SKILLS_DIR, { recursive: true });
138+
}
139+
mkdirSync(SKILLS_DIR, { recursive: true });
140+
};
141+
142+
const processComponent = async (mdxPath: string): Promise<void> => {
143+
const componentName = basename(mdxPath, '.mdx');
144+
const skillDir = join(SKILLS_DIR, `use-${componentName}-component`);
145+
const scriptsDir = join(skillDir, 'scripts');
146+
147+
const fileContent = await readFile(mdxPath, 'utf-8');
148+
const { data } = matter(fileContent);
149+
150+
const skillContent = transformMdx(fileContent, data.title, data.description);
151+
const examples = await findMatchingExamples(componentName);
152+
153+
mkdirSync(skillDir, { recursive: true });
154+
await writeFile(join(skillDir, 'SKILL.md'), skillContent);
155+
156+
if (examples.length > 0) {
157+
mkdirSync(scriptsDir, { recursive: true });
158+
for (const example of examples) {
159+
await copyFile(join(EXAMPLES_DIR, example), join(scriptsDir, example));
160+
}
161+
}
162+
163+
console.log(`Generated: use-${componentName}-component (${examples.length} examples)`);
164+
};
165+
166+
const main = async (): Promise<void> => {
167+
console.log('Generating skills from docs and examples...\n');
168+
169+
cleanSkillsDir();
170+
171+
const mdxFiles = await discoverMdxFiles(DOCS_DIR);
172+
console.log(`Found ${mdxFiles.length} MDX files\n`);
173+
174+
for (const mdxPath of mdxFiles) {
175+
await processComponent(mdxPath);
176+
}
177+
178+
console.log('\nDone!');
179+
};
180+
181+
main().catch(console.error);

packages/scripts/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"extends": "@repo/typescript-config/base.json",
4+
"compilerOptions": {
5+
"outDir": "dist",
6+
"rootDir": "src"
7+
},
8+
"include": ["src"]
9+
}

0 commit comments

Comments
 (0)