aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
224 lines • 7.62 kB
JavaScript
/**
* Lint Rule Loader
*
* Discovers and loads lint rulesets from installed frameworks.
* Each framework ships a `lint/` directory with a `ruleset.yaml`
* and individual rule YAML files.
*
* @issue #810
*/
import fs from 'fs';
import fsp from 'fs/promises';
import path from 'path';
/**
* Parse YAML frontmatter-style content (simple key: value parser)
* Handles the subset of YAML used in lint rule definitions.
*/
function parseSimpleYaml(content) {
const result = {};
const lines = content.split('\n');
let currentKey = '';
let currentArray = null;
let currentObject = null;
let inChecks = false;
for (const line of lines) {
const trimmed = line.trimEnd();
// Skip comments and empty lines
if (trimmed === '' || trimmed.startsWith('#'))
continue;
// Top-level key: value
const topMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
if (topMatch && !trimmed.startsWith(' ')) {
// Flush any pending array
if (currentArray && currentKey) {
result[currentKey] = currentArray;
currentArray = null;
}
currentKey = topMatch[1];
const value = topMatch[2].trim();
if (value === '') {
// Could be start of a nested structure
if (currentKey === 'checks') {
inChecks = true;
currentArray = [];
}
else if (currentKey === 'applies-to') {
currentObject = {};
result[currentKey] = currentObject;
}
}
else {
inChecks = false;
result[currentKey] = parseValue(value);
}
continue;
}
// Nested key under applies-to
if (currentObject && trimmed.match(/^\s{2}\w/)) {
const nestedMatch = trimmed.match(/^\s{2}(\w[\w-]*)\s*:\s*(.+)$/);
if (nestedMatch) {
currentObject[nestedMatch[1]] = parseValue(nestedMatch[2].trim());
continue;
}
}
// Array item in checks
if (inChecks && trimmed.match(/^\s{2}-\s/)) {
currentObject = {};
if (currentArray)
currentArray.push(currentObject);
const itemMatch = trimmed.match(/^\s{2}-\s+(\w[\w-]*)\s*:\s*(.+)$/);
if (itemMatch) {
currentObject[itemMatch[1]] = parseValue(itemMatch[2].trim());
}
continue;
}
// Properties of current check item
if (inChecks && currentObject && trimmed.match(/^\s{4}\w/)) {
const propMatch = trimmed.match(/^\s{4}(\w[\w-]*)\s*:\s*(.+)$/);
if (propMatch) {
const val = propMatch[2].trim();
// Handle inline arrays: [field1, field2, field3]
if (val.startsWith('[') && val.endsWith(']')) {
currentObject[propMatch[1]] = val
.slice(1, -1)
.split(',')
.map((s) => s.trim());
}
else {
currentObject[propMatch[1]] = parseValue(val);
}
continue;
}
}
}
// Flush trailing array
if (currentArray && currentKey) {
result[currentKey] = currentArray;
}
return result;
}
function parseValue(val) {
// Boolean
if (val === 'true')
return true;
if (val === 'false')
return false;
// Number
if (/^\d+(\.\d+)?$/.test(val))
return Number(val);
// Inline array
if (val.startsWith('[') && val.endsWith(']')) {
return val
.slice(1, -1)
.split(',')
.map(s => s.trim().replace(/^["']|["']$/g, ''));
}
// Strip quotes and unescape YAML double-quote sequences
const unquoted = val.replace(/^["']|["']$/g, '');
return unquoted.replace(/\\\\/g, '\\');
}
/**
* Load a single lint rule from a YAML file
*/
export async function loadRule(filePath) {
const content = await fsp.readFile(filePath, 'utf8');
const parsed = parseSimpleYaml(content);
const appliesTo = parsed['applies-to'];
return {
id: String(parsed.id || ''),
name: String(parsed.name || ''),
description: String(parsed.description || ''),
severity: parsed.severity || 'warn',
appliesTo: {
glob: appliesTo?.glob || '**/*',
},
checks: parsed.checks || [],
};
}
/**
* Load a ruleset and all its rules from a framework's lint/ directory
*/
export async function loadRuleset(lintDir, frameworkId) {
const rulesetPath = path.join(lintDir, 'ruleset.yaml');
if (!fs.existsSync(rulesetPath))
return null;
const content = await fsp.readFile(rulesetPath, 'utf8');
const parsed = parseSimpleYaml(content);
// Load individual rule files
const rules = [];
const entries = await fsp.readdir(lintDir);
for (const entry of entries) {
if (entry === 'ruleset.yaml')
continue;
if (!entry.endsWith('.yaml') && !entry.endsWith('.yml'))
continue;
try {
const rule = await loadRule(path.join(lintDir, entry));
rules.push(rule);
}
catch {
// Skip malformed rule files
}
}
return {
id: String(parsed.id || frameworkId),
name: String(parsed.name || frameworkId),
description: String(parsed.description || ''),
framework: frameworkId,
version: String(parsed.version || '1.0.0'),
rules,
};
}
/**
* Discover all available rulesets from installed frameworks
*
* Looks in two locations:
* 1. Framework source: agentic/code/frameworks/{id}/lint/
* 2. Deployed frameworks: .aiwg/frameworks/{id}/lint/
*/
export async function discoverRulesets(cwd, frameworkRoot) {
const rulesets = [];
// Check framework source directories
const frameworksDir = path.join(frameworkRoot, 'agentic', 'code', 'frameworks');
if (fs.existsSync(frameworksDir)) {
try {
const frameworks = await fsp.readdir(frameworksDir);
for (const fw of frameworks) {
const lintDir = path.join(frameworksDir, fw, 'lint');
if (fs.existsSync(lintDir)) {
const ruleset = await loadRuleset(lintDir, fw);
if (ruleset)
rulesets.push(ruleset);
}
}
}
catch {
// Skip on error
}
}
// Also check .aiwg/frameworks/ for deployed rulesets
const deployedDir = path.join(cwd, '.aiwg', 'frameworks');
if (fs.existsSync(deployedDir)) {
try {
const frameworks = await fsp.readdir(deployedDir);
for (const fw of frameworks) {
if (fw === 'registry.json')
continue;
const lintDir = path.join(deployedDir, fw, 'lint');
if (fs.existsSync(lintDir)) {
// Skip if already loaded from source
if (!rulesets.find(r => r.id === fw)) {
const ruleset = await loadRuleset(lintDir, fw);
if (ruleset)
rulesets.push(ruleset);
}
}
}
}
catch {
// Skip on error
}
}
return rulesets;
}
//# sourceMappingURL=loader.js.map