agent-rules
Version:
Rules and instructions for agentic coding tools like Cursor, Claude CLI, Gemini CLI, Qodo, Cline and more
180 lines (148 loc) • 6.45 kB
text/typescript
import path from 'node:path'
import fs from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { debuglog } from 'node:util'
type ScaffoldInstructions = {
aiApp: string;
codeLanguage: string;
codeTopic: string;
}
type AiAppConfig = {
directory: string;
filesSuffix: string;
}
type AiAppsMap = {
[key: string]: AiAppConfig;
}
const debug = debuglog('agent-rules')
const templateRoot = '__template__'
const mapAiAppsToDirectories: AiAppsMap = {
'github-copilot': {
directory: '.github/instructions',
filesSuffix: '.instructions.md',
}
}
// Simple path resolution because templates are copied to dist during build
// see package.json scripts for the build process but we also want to support
// running from source in dev `npm run start` where this file lives in src/
function resolvePackageRootDirectoryForTemplates (): string {
let guessedDirName: string = ''
try {
if (typeof import.meta !== 'undefined' && import.meta.url) {
// ESM environment - templates are in dist/__template__
const __filename = fileURLToPath(import.meta.url)
guessedDirName = path.dirname(__filename)
} else {
guessedDirName = __dirname
}
} catch (error) {
// CJS fallback - assume we're in a distributed package
// In CJS, we don't have import.meta, so use __dirname
guessedDirName = __dirname
}
// If we're in dist/bin, go up one level to dist
// If we're already in dist, stay there
if (guessedDirName.endsWith('src')) {
// If we're in src, we need to go up one level to root
return path.resolve(guessedDirName, '..')
} else if (guessedDirName.endsWith('dist/bin') || guessedDirName.endsWith('dist\\bin')) {
return path.resolve(guessedDirName, '..')
} else {
return guessedDirName
}
}
export function getAiAppDirectory (aiApp: string): AiAppConfig {
// eslint-disable-next-line security/detect-object-injection
const app = Object.hasOwn(mapAiAppsToDirectories, aiApp) ? mapAiAppsToDirectories[aiApp] : null
if (!app) {
throw new Error(`AI App "${aiApp}" is not supported.`)
}
return app
}
export async function resolveTemplateDirectory (scaffoldInstructions: ScaffoldInstructions): Promise<string> {
const { codeLanguage, codeTopic } = scaffoldInstructions
const currentFileDirectory = resolvePackageRootDirectoryForTemplates()
const templateDirectory = path.join(currentFileDirectory, templateRoot, codeLanguage, codeTopic)
const resolvedTemplateDirectory = path.resolve(templateDirectory)
try {
const templateStats = await fs.stat(resolvedTemplateDirectory)
if (!templateStats.isDirectory()) {
throw new Error(`Template directory is not a directory: ${resolvedTemplateDirectory}`)
}
} catch (error) {
throw new Error(`Template directory not found: ${resolvedTemplateDirectory}`)
}
return resolvedTemplateDirectory
}
async function createTargetDirectory (directory: string): Promise<string> {
const resolvedTargetDirectory = path.resolve(directory)
await fs.mkdir(resolvedTargetDirectory, { recursive: true })
return resolvedTargetDirectory
}
function generateTargetFileName (templateFileName: string, filesSuffix: string): string {
const parsedFile = path.parse(templateFileName)
let baseName = parsedFile.name
// If the template file already has the suffix in its name, remove it to avoid duplication
if (baseName.endsWith('.instructions')) {
baseName = baseName.replace(/\.instructions$/, '')
}
return `${baseName}${filesSuffix}`
}
function validateTargetPath (targetFilePath: string, resolvedTargetDirectory: string): string {
const resolvedTargetFilePath = path.resolve(targetFilePath)
if (!resolvedTargetFilePath.startsWith(resolvedTargetDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`)
}
return resolvedTargetFilePath
}
async function copyTemplateFile (
templateFilePath: string,
targetFilePath: string,
resolvedTargetDirectory: string,
filesSuffix: string
): Promise<void> {
const sanitizedTemplateFile = path.basename(templateFilePath)
const fullTemplatePath = path.join(path.dirname(templateFilePath), sanitizedTemplateFile)
debug('Processing template file:', sanitizedTemplateFile)
try {
const stat = await fs.stat(fullTemplatePath)
// Only process files, not directories
if (stat.isFile()) {
const targetFileName = generateTargetFileName(sanitizedTemplateFile, filesSuffix)
const targetPath = path.join(targetFilePath, targetFileName)
const resolvedTargetFilePath = validateTargetPath(targetPath, resolvedTargetDirectory)
debug('Writing template file to target path:', resolvedTargetFilePath)
// Read the template file content
const templateContent = await fs.readFile(fullTemplatePath, 'utf-8')
// Write to the target location
await fs.writeFile(resolvedTargetFilePath, templateContent, 'utf-8')
}
} catch (error) {
console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
async function copyTemplateFiles (
resolvedTemplateDirectory: string,
resolvedTargetDirectory: string,
filesSuffix: string
): Promise<void> {
const templateFiles = await fs.readdir(resolvedTemplateDirectory)
for (const templateFile of templateFiles) {
const templateFilePath = path.join(resolvedTemplateDirectory, templateFile)
await copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix)
}
}
export async function scaffoldAiAppInstructions (scaffoldInstructions: ScaffoldInstructions): Promise<void> {
const { aiApp, codeLanguage, codeTopic } = scaffoldInstructions
if (!aiApp || !codeLanguage || !codeTopic) {
throw new Error('Scaffold instructions must include aiApp and all other template choices.')
}
const aiAppConfig = getAiAppDirectory(aiApp)
const { directory, filesSuffix } = aiAppConfig
debug(`Scaffolding AI App instructions in directory: ${directory} with files suffix: ${filesSuffix}`)
// Resolve and validate template directory, then ensure target directory exists
const resolvedTemplateDirectory = await resolveTemplateDirectory(scaffoldInstructions)
const resolvedTargetDirectory = await createTargetDirectory(directory)
// Copy all template files to the target directory
await copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix)
}