1
0
mirror of synced 2025-12-19 09:57:42 -05:00
Files
docs/src/workflows/writers-help-metadata.ts

216 lines
6.3 KiB
TypeScript

#!/usr/bin/env tsx
import { readFileSync } from 'fs'
import { glob } from 'glob'
import path from 'path'
import { fileURLToPath } from 'url'
const __scriptname = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__scriptname)
const PURPOSE_STRING = '@purpose Writer tool'
const DESCRIPTION_STRING = '@description'
const DESCRIPTION_REGEX = new RegExp(`${DESCRIPTION_STRING}\\s+(.+)`)
interface WriterTool {
name: string
description: string
priority?: number // Lower numbers = higher priority
}
interface WriterToolsCollection {
[category: string]: WriterTool[]
}
interface ScriptMetadata {
isWriterTool?: boolean
category?: string
description?: string
}
// Manual entries for scripts that aren't TypeScript files with metadata
const MANUAL_ENTRIES: WriterToolsCollection = {
'Validation and formatting': [
{ name: 'prettier', description: 'Format markdown, YAML, and other files' },
],
Development: [
{ name: 'dev', description: 'Start local development server' },
{ name: 'build', description: 'Build the application' },
],
}
async function discoverWriterTools(): Promise<WriterToolsCollection> {
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json')
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
const tools: WriterToolsCollection = { ...MANUAL_ENTRIES } // Start with manual entries
// First get all files
const allFiles = await glob('src/**/*', {
cwd: path.join(__dirname, '..', '..'),
absolute: true,
ignore: ['**/node_modules/**', '**/tests/**', '**/test/**', '**/.*'],
})
// Then filter for .ts, .js, .sh scripts
const scriptFiles = allFiles.filter((file) => {
if (file === __scriptname) return false // skip the current file
const ext = path.extname(file)
if (['.ts', '.js', '.sh'].includes(ext)) return true
// For extensionless files, check if they're executable or have shebang
if (ext === '') {
try {
const content = readFileSync(file, 'utf8')
return content.startsWith('#!/bin/bash') || content.startsWith('#!/usr/bin/env bash')
} catch {
return false
}
}
return false
})
for (const filePath of scriptFiles) {
try {
const relativePath = path.relative(process.cwd(), filePath)
const content = readFileSync(filePath, 'utf8')
const metadata = extractMetadata(content)
if (metadata.isWriterTool) {
metadata.category = getCategory(relativePath)
// Find corresponding npm script
const scriptName = findScriptName(packageJson.scripts, relativePath)
if (scriptName) {
if (!tools[metadata.category]) tools[metadata.category] = []
// Check if not already added manually
const exists = tools[metadata.category].some((tool) => tool.name === scriptName)
if (!exists) {
tools[metadata.category].push({
name: scriptName,
description: metadata.description || `${scriptName} tool`,
})
}
}
}
} catch {
// Skip files that can't be read
continue
}
}
return tools
}
function extractMetadata(content: string): ScriptMetadata {
const metadata: ScriptMetadata = {}
const lines = content.split('\n').slice(0, 20) // Only check first 20 lines
for (const line of lines) {
if (line.includes(PURPOSE_STRING)) {
metadata.isWriterTool = true
}
if (line.includes(DESCRIPTION_STRING)) {
// Extract description from line like "@description Add content type frontmatter to articles"
const match = line.match(DESCRIPTION_REGEX)
if (match) {
metadata.description = match[1].trim()
}
}
}
return metadata
}
// Convert the DIR in src/DIR/ to a title-cased category name
// E.g. src/secret-scanning becomes Secret Scanning
function getCategory(relativePath: string): string {
const directory = relativePath.split(path.sep)[1]
const category = directory
.split('-')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ')
// Clarify this one category
return category.replace('Content Render', 'Content Tasks')
}
function findScriptName(scripts: Record<string, string>, relativePath: string): string | null {
for (const [scriptName, command] of Object.entries(scripts)) {
// Check if the command includes this file path
if (command.includes(relativePath)) {
return scriptName
}
// Also check for simplified paths without the src/ prefix
const simplifiedPath = relativePath.replace(/^src\//, '')
if (command.includes(simplifiedPath)) {
return scriptName
}
}
return null
}
function prioritizeOrder(tools: WriterToolsCollection) {
// Define priorities for specific tools
const priorities = {
'move-content': 1,
'cta-builder': 2,
'lint-content': 1,
docstat: 1,
dev: 1,
}
// Assign priorities to discovered tools
Object.values(tools)
.flat()
.forEach((tool) => {
if (priorities[tool.name as keyof typeof priorities]) {
tool.priority = priorities[tool.name as keyof typeof priorities]
}
})
// Sort each category by priority, then alphabetically
Object.keys(tools).forEach((category) => {
tools[category].sort((a, b) => {
// Items with priority come first
if (a.priority !== undefined && b.priority === undefined) return -1
if (a.priority === undefined && b.priority !== undefined) return 1
// Both have priority: sort by priority value
if (a.priority !== undefined && b.priority !== undefined) {
return a.priority - b.priority
}
// Neither has priority: sort alphabetically
return a.name.localeCompare(b.name)
})
})
return tools
}
async function main(): Promise<void> {
console.log('For more info, run a command with "-- --help".\n')
const tools = prioritizeOrder(await discoverWriterTools())
Object.entries(tools).forEach(([category, scripts]) => {
console.log(`${category}:`)
scripts.forEach((script) => {
const padding = ' '.repeat(Math.max(0, 34 - script.name.length))
console.log(` npm run ${script.name}${padding}# ${script.description}`)
})
console.log('')
})
}
if (import.meta.url === `file://${process.argv[1]}`) {
try {
await main()
} catch (error) {
console.error(error)
process.exit(1)
}
}