snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
255
skills/skill-creator/scripts/init-skill.ts
Normal file
255
skills/skill-creator/scripts/init-skill.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Skill Initializer - Creates a new skill from template
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx init-skill.ts <skill-name> --path <path>
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx init-skill.ts my-new-skill --path skills/public
|
||||
* npx tsx init-skill.ts my-api-helper --path skills/private
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const SKILL_TEMPLATE = `---
|
||||
name: {{skill_name}}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {{skill_title}}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## Structuring This Skill
|
||||
|
||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||
|
||||
**1. Workflow-Based** (best for sequential processes)
|
||||
- Works well when there are clear step-by-step procedures
|
||||
- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing"
|
||||
- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
|
||||
|
||||
**2. Task-Based** (best for tool collections)
|
||||
- Works well when the skill offers different operations/capabilities
|
||||
- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text"
|
||||
- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
|
||||
|
||||
**3. Reference/Guidelines** (best for standards or specifications)
|
||||
- Works well for brand guidelines, coding standards, or requirements
|
||||
- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features"
|
||||
- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
|
||||
|
||||
**4. Capabilities-Based** (best for integrated systems)
|
||||
- Works well when the skill provides multiple interrelated features
|
||||
- Example: Product Management with "Core Capabilities" → numbered capability list
|
||||
- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
|
||||
|
||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||
|
||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||
|
||||
## [TODO: Replace with the first main section based on chosen structure]
|
||||
|
||||
[TODO: Add content here. See examples in existing skills:
|
||||
- Code samples for technical skills
|
||||
- Decision trees for complex workflows
|
||||
- Concrete examples with realistic user requests
|
||||
- References to scripts/templates/references as needed]
|
||||
|
||||
## Resources
|
||||
|
||||
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
|
||||
|
||||
### scripts/
|
||||
Executable code (TypeScript/Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||
|
||||
### references/
|
||||
Documentation and reference material intended to be loaded into context to inform the agent's process and thinking.
|
||||
|
||||
### assets/
|
||||
Files not intended to be loaded into context, but rather used within the output the agent produces.
|
||||
|
||||
---
|
||||
|
||||
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
|
||||
`
|
||||
|
||||
const EXAMPLE_SCRIPT = `#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Example helper script for {{skill_name}}
|
||||
*
|
||||
* This is a placeholder script that can be executed directly.
|
||||
* Replace with actual implementation or delete if not needed.
|
||||
*/
|
||||
|
||||
function main() {
|
||||
console.log('This is an example script for {{skill_name}}')
|
||||
// TODO: Add actual script logic here
|
||||
}
|
||||
|
||||
main()
|
||||
`
|
||||
|
||||
const EXAMPLE_REFERENCE = `# Reference Documentation for {{skill_title}}
|
||||
|
||||
This is a placeholder for detailed reference documentation.
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
## When Reference Docs Are Useful
|
||||
|
||||
Reference docs are ideal for:
|
||||
- Comprehensive API documentation
|
||||
- Detailed workflow guides
|
||||
- Complex multi-step processes
|
||||
- Information too lengthy for main SKILL.md
|
||||
- Content that's only needed for specific use cases
|
||||
`
|
||||
|
||||
const EXAMPLE_ASSET = `# Example Asset File
|
||||
|
||||
This placeholder represents where asset files would be stored.
|
||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT intended to be loaded into context, but rather used within
|
||||
the output the agent produces.
|
||||
|
||||
## Common Asset Types
|
||||
|
||||
- Templates: .pptx, .docx, boilerplate directories
|
||||
- Images: .png, .jpg, .svg, .gif
|
||||
- Fonts: .ttf, .otf, .woff, .woff2
|
||||
- Boilerplate code: Project directories, starter files
|
||||
`
|
||||
|
||||
function titleCase(skillName: string): string {
|
||||
return skillName
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function fillTemplate(template: string, skillName: string, skillTitle: string): string {
|
||||
return template
|
||||
.replace(/\{\{skill_name\}\}/g, skillName)
|
||||
.replace(/\{\{skill_title\}\}/g, skillTitle)
|
||||
}
|
||||
|
||||
function validateSkillName(skillName: string): string | null {
|
||||
// Check for valid hyphen-case format
|
||||
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
||||
return `Name '${skillName}' should be hyphen-case (lowercase letters, digits, and hyphens only)`
|
||||
}
|
||||
if (skillName.startsWith('-') || skillName.endsWith('-') || skillName.includes('--')) {
|
||||
return `Name '${skillName}' cannot start/end with hyphen or contain consecutive hyphens`
|
||||
}
|
||||
if (skillName.length > 64) {
|
||||
return `Name is too long (${skillName.length} characters). Maximum is 64 characters.`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function initSkill(skillName: string, basePath: string): string | null {
|
||||
// Validate skill name format
|
||||
const nameError = validateSkillName(skillName)
|
||||
if (nameError) {
|
||||
console.log(`❌ Error: ${nameError}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const skillDir = path.resolve(basePath, skillName)
|
||||
|
||||
// Check if directory already exists
|
||||
if (fs.existsSync(skillDir)) {
|
||||
console.log(`❌ Error: Skill directory already exists: ${skillDir}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const skillTitle = titleCase(skillName)
|
||||
|
||||
let dirCreated = false
|
||||
try {
|
||||
// Create skill directory
|
||||
fs.mkdirSync(skillDir, { recursive: true })
|
||||
dirCreated = true
|
||||
console.log(`✅ Created skill directory: ${skillDir}`)
|
||||
|
||||
// Create SKILL.md
|
||||
const skillContent = fillTemplate(SKILL_TEMPLATE, skillName, skillTitle)
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent)
|
||||
console.log('✅ Created SKILL.md')
|
||||
|
||||
// Create scripts/ directory with example
|
||||
const scriptsDir = path.join(skillDir, 'scripts')
|
||||
fs.mkdirSync(scriptsDir)
|
||||
fs.writeFileSync(
|
||||
path.join(scriptsDir, 'example.ts'),
|
||||
fillTemplate(EXAMPLE_SCRIPT, skillName, skillTitle)
|
||||
)
|
||||
console.log('✅ Created scripts/example.ts')
|
||||
|
||||
// Create references/ directory with example
|
||||
const referencesDir = path.join(skillDir, 'references')
|
||||
fs.mkdirSync(referencesDir)
|
||||
fs.writeFileSync(
|
||||
path.join(referencesDir, 'api-reference.md'),
|
||||
fillTemplate(EXAMPLE_REFERENCE, skillName, skillTitle)
|
||||
)
|
||||
console.log('✅ Created references/api-reference.md')
|
||||
|
||||
// Create assets/ directory with example
|
||||
const assetsDir = path.join(skillDir, 'assets')
|
||||
fs.mkdirSync(assetsDir)
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'example-asset.txt'),
|
||||
fillTemplate(EXAMPLE_ASSET, skillName, skillTitle)
|
||||
)
|
||||
console.log('✅ Created assets/example-asset.txt')
|
||||
|
||||
console.log(`\n✅ Skill '${skillName}' initialized successfully at ${skillDir}`)
|
||||
console.log('\nNext steps:')
|
||||
console.log('1. Edit SKILL.md to complete the TODO items and update the description')
|
||||
console.log('2. Customize or delete the example files in scripts/, references/, and assets/')
|
||||
console.log('3. Run the validator when ready to check the skill structure')
|
||||
|
||||
return skillDir
|
||||
} catch (err) {
|
||||
// Clean up partially created directory on failure
|
||||
if (dirCreated && fs.existsSync(skillDir)) {
|
||||
try {
|
||||
fs.rmSync(skillDir, { recursive: true })
|
||||
console.log(`🧹 Cleaned up partial directory: ${skillDir}`)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
console.log(`❌ Error: ${err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length < 3 || args[1] !== '--path') {
|
||||
console.log('Usage: npx tsx init-skill.ts <skill-name> --path <path>')
|
||||
console.log('\nSkill name requirements:')
|
||||
console.log(" - Hyphen-case identifier (e.g., 'data-analyzer')")
|
||||
console.log(' - Lowercase letters, digits, and hyphens only')
|
||||
console.log(' - Max 40 characters')
|
||||
console.log('\nExamples:')
|
||||
console.log(' npx tsx init-skill.ts my-new-skill --path skills')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const skillName = args[0]
|
||||
const basePath = args[2]
|
||||
|
||||
console.log(`🚀 Initializing skill: ${skillName}`)
|
||||
console.log(` Location: ${basePath}`)
|
||||
console.log()
|
||||
|
||||
const result = initSkill(skillName, basePath)
|
||||
process.exit(result ? 0 : 1)
|
||||
152
skills/skill-creator/scripts/package-skill.ts
Normal file
152
skills/skill-creator/scripts/package-skill.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Skill Packager - Creates a distributable .skill file of a skill folder
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx package-skill.ts <path/to/skill-folder> [output-directory]
|
||||
*
|
||||
* Example:
|
||||
* npx tsx package-skill.ts skills/my-skill
|
||||
* npx tsx package-skill.ts skills/my-skill ./dist
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import { validateSkill } from './quick-validate'
|
||||
|
||||
function getAllFiles(dir: string, baseDir: string = dir): string[] {
|
||||
const files: string[] = []
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...getAllFiles(fullPath, baseDir))
|
||||
} else {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
function packageSkill(skillPath: string, outputDir?: string): string | null {
|
||||
const resolvedSkillPath = path.resolve(skillPath)
|
||||
|
||||
// Validate skill folder exists
|
||||
if (!fs.existsSync(resolvedSkillPath)) {
|
||||
console.log(`❌ Error: Skill folder not found: ${resolvedSkillPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!fs.statSync(resolvedSkillPath).isDirectory()) {
|
||||
console.log(`❌ Error: Path is not a directory: ${resolvedSkillPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Validate SKILL.md exists
|
||||
const skillMdPath = path.join(resolvedSkillPath, 'SKILL.md')
|
||||
if (!fs.existsSync(skillMdPath)) {
|
||||
console.log(`❌ Error: SKILL.md not found in ${resolvedSkillPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Run validation before packaging
|
||||
console.log('🔍 Validating skill...')
|
||||
const { valid, message } = validateSkill(resolvedSkillPath)
|
||||
if (!valid) {
|
||||
console.log(`❌ Validation failed: ${message}`)
|
||||
console.log(' Please fix the validation errors before packaging.')
|
||||
return null
|
||||
}
|
||||
console.log(`✅ ${message}\n`)
|
||||
|
||||
// Determine output location
|
||||
const skillName = path.basename(resolvedSkillPath)
|
||||
|
||||
// Validate directory name is safe for shell commands (same rules as skill names)
|
||||
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
||||
console.log(
|
||||
`❌ Error: Directory name '${skillName}' contains unsafe characters. Use only lowercase letters, digits, and hyphens.`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const resolvedOutputDir = outputDir ? path.resolve(outputDir) : process.cwd()
|
||||
|
||||
if (!fs.existsSync(resolvedOutputDir)) {
|
||||
fs.mkdirSync(resolvedOutputDir, { recursive: true })
|
||||
}
|
||||
|
||||
const skillFilename = path.join(resolvedOutputDir, `${skillName}.skill`)
|
||||
|
||||
let tempDir: string | null = null
|
||||
try {
|
||||
// Get all files in the skill directory
|
||||
const files = getAllFiles(resolvedSkillPath)
|
||||
|
||||
// Create a temporary directory for the zip structure (cross-platform)
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-package-'))
|
||||
const tempSkillDir = path.join(tempDir, skillName)
|
||||
fs.mkdirSync(tempSkillDir, { recursive: true })
|
||||
|
||||
// Copy all files maintaining structure
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(resolvedSkillPath, file)
|
||||
const destPath = path.join(tempSkillDir, relativePath)
|
||||
const destDir = path.dirname(destPath)
|
||||
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.copyFileSync(file, destPath)
|
||||
console.log(` Added: ${skillName}/${relativePath}`)
|
||||
}
|
||||
|
||||
// Create zip file using system zip command
|
||||
const zipPath = path.resolve(skillFilename)
|
||||
execSync(`cd "${tempDir}" && zip -r "${zipPath}" "${skillName}"`, { stdio: 'pipe' })
|
||||
|
||||
// Cleanup temp directory
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
|
||||
console.log(`\n✅ Successfully packaged skill to: ${skillFilename}`)
|
||||
return skillFilename
|
||||
} catch (err) {
|
||||
// Cleanup temp directory on error
|
||||
if (tempDir && fs.existsSync(tempDir)) {
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
console.log(`❌ Error creating .skill file: ${err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length < 1) {
|
||||
console.log('Usage: npx tsx package-skill.ts <path/to/skill-folder> [output-directory]')
|
||||
console.log('\nExample:')
|
||||
console.log(' npx tsx package-skill.ts skills/my-skill')
|
||||
console.log(' npx tsx package-skill.ts skills/my-skill ./dist')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const skillPath = args[0]
|
||||
const outputDir = args[1]
|
||||
|
||||
console.log(`📦 Packaging skill: ${skillPath}`)
|
||||
if (outputDir) {
|
||||
console.log(` Output directory: ${outputDir}`)
|
||||
}
|
||||
console.log()
|
||||
|
||||
const result = packageSkill(skillPath, outputDir)
|
||||
process.exit(result ? 0 : 1)
|
||||
152
skills/skill-creator/scripts/quick-validate.ts
Normal file
152
skills/skill-creator/scripts/quick-validate.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Quick validation script for skills - minimal version
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
const ALLOWED_PROPERTIES = new Set([
|
||||
'name',
|
||||
'description',
|
||||
'license',
|
||||
'compatibility',
|
||||
'allowed-tools',
|
||||
'metadata',
|
||||
])
|
||||
|
||||
function parseSimpleYaml(text: string): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
for (const line of text.split('\n')) {
|
||||
const match = line.match(/^(\S+):\s*(.*)$/)
|
||||
if (match) {
|
||||
const [, key, value] = match
|
||||
result[key] = value.trim()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function validateSkill(skillPath: string): ValidationResult {
|
||||
const skillMdPath = path.join(skillPath, 'SKILL.md')
|
||||
|
||||
// Check SKILL.md exists
|
||||
if (!fs.existsSync(skillMdPath)) {
|
||||
return { valid: false, message: 'SKILL.md not found' }
|
||||
}
|
||||
|
||||
// Read and validate frontmatter
|
||||
const content = fs.readFileSync(skillMdPath, 'utf-8')
|
||||
if (!content.startsWith('---')) {
|
||||
return { valid: false, message: 'No YAML frontmatter found' }
|
||||
}
|
||||
|
||||
// Extract frontmatter (handle both LF and CRLF line endings)
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
||||
if (!match) {
|
||||
return { valid: false, message: 'Invalid frontmatter format' }
|
||||
}
|
||||
|
||||
const frontmatterText = match[1]
|
||||
const frontmatter = parseSimpleYaml(frontmatterText)
|
||||
|
||||
// Check for unexpected properties
|
||||
const unexpectedKeys = Object.keys(frontmatter).filter((k) => !ALLOWED_PROPERTIES.has(k))
|
||||
if (unexpectedKeys.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Unexpected key(s) in SKILL.md frontmatter: ${unexpectedKeys.sort().join(', ')}. Allowed properties are: ${[...ALLOWED_PROPERTIES].sort().join(', ')}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!frontmatter.name) {
|
||||
return { valid: false, message: "Missing 'name' in frontmatter" }
|
||||
}
|
||||
if (!frontmatter.description) {
|
||||
return { valid: false, message: "Missing 'description' in frontmatter" }
|
||||
}
|
||||
|
||||
// Validate name
|
||||
const name = frontmatter.name.trim()
|
||||
if (!name) {
|
||||
return { valid: false, message: 'Name cannot be empty or whitespace-only' }
|
||||
}
|
||||
{
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name '${name}' should be hyphen-case (lowercase letters, digits, and hyphens only)`,
|
||||
}
|
||||
}
|
||||
if (name.startsWith('-') || name.endsWith('-') || name.includes('--')) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name '${name}' cannot start/end with hyphen or contain consecutive hyphens`,
|
||||
}
|
||||
}
|
||||
if (name.length > 64) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name is too long (${name.length} characters). Maximum is 64 characters.`,
|
||||
}
|
||||
}
|
||||
const directoryName = path.basename(path.resolve(skillPath))
|
||||
if (name !== directoryName) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name '${name}' must match parent directory name '${directoryName}'.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate description
|
||||
const description = frontmatter.description.trim()
|
||||
if (!description) {
|
||||
return { valid: false, message: 'Description cannot be empty or whitespace-only' }
|
||||
}
|
||||
{
|
||||
if (description.includes('<') || description.includes('>')) {
|
||||
return { valid: false, message: 'Description cannot contain angle brackets (< or >)' }
|
||||
}
|
||||
if (description.length > 1024) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Description is too long (${description.length} characters). Maximum is 1024 characters.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compatibility = frontmatter.compatibility?.trim()
|
||||
if (compatibility !== undefined) {
|
||||
if (!compatibility) {
|
||||
return { valid: false, message: 'Compatibility cannot be empty or whitespace-only' }
|
||||
}
|
||||
if (compatibility.length > 500) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Compatibility is too long (${compatibility.length} characters). Maximum is 500 characters.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, message: 'Skill is valid!' }
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length !== 1) {
|
||||
process.stdout.write('Usage: npx tsx quick-validate.ts <skill_directory>\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const { valid, message } = validateSkill(args[0])
|
||||
process.stdout.write(`${message}\n`)
|
||||
process.exit(valid ? 0 : 1)
|
||||
}
|
||||
Reference in New Issue
Block a user