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
339 lines • 13.2 kB
JavaScript
/**
* Local Registry Adapter
*
* Scans agentic/code/ for skill manifests and SKILL.md files.
* Also checks deployed skills in the current project's provider directory.
*
* @implements #539
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
const AIWG_ROOT = process.env.AIWG_ROOT || path.resolve(_scriptDir, '../../../');
/**
* Parse YAML-like frontmatter from SKILL.md content
*/
function parseFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match)
return {};
const result = {};
for (const line of match[1].split('\n')) {
const kv = line.match(/^(\w+):\s*(.+)$/);
if (kv) {
result[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
}
}
return result;
}
/**
* Parse platforms from frontmatter value like "[claude-code, hermes, openclaw]"
*/
function parsePlatforms(value) {
if (!value)
return [];
return value
.replace(/^\[|\]$/g, '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}
/**
* Extract triggers section from SKILL.md content
*/
function extractTriggers(content) {
const triggerSection = content.match(/## Triggers\n\n([\s\S]*?)(?:\n##|\n$)/);
if (!triggerSection)
return [];
return triggerSection[1]
.split('\n')
.filter((line) => line.startsWith('- '))
.map((line) => line.replace(/^- ["']?|["']?$/g, '').trim())
.filter(Boolean);
}
/**
* Recursively find all manifest.json files under a directory
*/
function findManifests(dir) {
const results = [];
if (!fs.existsSync(dir))
return results;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Check for skills/manifest.json pattern
const manifestPath = path.join(fullPath, 'skills', 'manifest.json');
if (fs.existsSync(manifestPath)) {
results.push(manifestPath);
}
// Recurse into subdirectories
results.push(...findManifests(fullPath));
}
}
return results;
}
/**
* Deduplicate manifests — keep the shallowest path for each manifest
*/
function deduplicateManifests(paths) {
const seen = new Set();
return paths.filter((p) => {
if (seen.has(p))
return false;
seen.add(p);
return true;
});
}
export class LocalAdapter {
id = 'local';
name = 'Local (AIWG Installation)';
cachedSkills = null;
async isAvailable() {
return fs.existsSync(path.join(AIWG_ROOT, 'agentic', 'code'));
}
async list() {
if (this.cachedSkills)
return this.cachedSkills;
const skills = [];
const agenticDir = path.join(AIWG_ROOT, 'agentic', 'code');
if (!fs.existsSync(agenticDir))
return skills;
// Find all manifest.json files in frameworks/ and addons/
const frameworksDir = path.join(agenticDir, 'frameworks');
const addonsDir = path.join(agenticDir, 'addons');
const manifestPaths = deduplicateManifests([
...findManifests(frameworksDir),
...findManifests(addonsDir),
]);
for (const manifestPath of manifestPaths) {
try {
const raw = fs.readFileSync(manifestPath, 'utf-8');
const manifest = JSON.parse(raw);
const skillsDir = path.dirname(manifestPath);
// Derive package name from path
// e.g., agentic/code/frameworks/sdlc-complete/skills/manifest.json → sdlc-complete
const parts = manifestPath.split(path.sep);
const frameworkIdx = parts.indexOf('frameworks');
const addonIdx = parts.indexOf('addons');
const packageName = frameworkIdx >= 0
? parts[frameworkIdx + 1]
: addonIdx >= 0
? parts[addonIdx + 1]
: manifest.name;
for (const skill of manifest.skills) {
// Try to read SKILL.md for platform info
const skillMdPath = path.join(skillsDir, skill.name, 'SKILL.md');
let platforms = [];
if (fs.existsSync(skillMdPath)) {
const content = fs.readFileSync(skillMdPath, 'utf-8');
const fm = parseFrontmatter(content);
platforms = parsePlatforms(fm.platforms);
}
skills.push({
name: skill.name,
description: skill.description,
source: 'local',
package: packageName,
platforms,
installed: true,
});
}
}
catch {
// Skip malformed manifests
}
}
// Also scan for SKILL.md files not in any manifest (standalone skills)
const skillMdPaths = this.findSkillMds(agenticDir);
const knownNames = new Set(skills.map((s) => s.name));
for (const skillMdPath of skillMdPaths) {
const skillName = path.basename(path.dirname(skillMdPath));
if (knownNames.has(skillName))
continue;
try {
const content = fs.readFileSync(skillMdPath, 'utf-8');
const fm = parseFrontmatter(content);
const platforms = parsePlatforms(fm.platforms);
// Extract description from first paragraph after frontmatter
const bodyMatch = content.match(/---\n\n# .+\n\n(.+)/);
const description = bodyMatch
? bodyMatch[1].slice(0, 120)
: `Skill: ${skillName}`;
// Derive package
const parts = skillMdPath.split(path.sep);
const frameworkIdx = parts.indexOf('frameworks');
const addonIdx = parts.indexOf('addons');
const packageName = frameworkIdx >= 0
? parts[frameworkIdx + 1]
: addonIdx >= 0
? parts[addonIdx + 1]
: 'unknown';
skills.push({
name: skillName,
description,
source: 'local',
package: packageName,
platforms,
installed: true,
});
}
catch {
// Skip unreadable files
}
}
this.cachedSkills = skills;
return skills;
}
async search(query) {
const all = await this.list();
const q = query.toLowerCase();
return all.filter((s) => s.name.toLowerCase().includes(q) ||
s.description.toLowerCase().includes(q) ||
(s.package && s.package.toLowerCase().includes(q)));
}
async info(name) {
const all = await this.list();
const match = all.find((s) => s.name === name);
if (!match)
return undefined;
// Find the SKILL.md for full details
const agenticDir = path.join(AIWG_ROOT, 'agentic', 'code');
const skillMdPaths = this.findSkillMds(agenticDir);
const skillMdPath = skillMdPaths.find((p) => path.basename(path.dirname(p)) === name);
const details = { ...match };
if (skillMdPath) {
const content = fs.readFileSync(skillMdPath, 'utf-8');
details.path = skillMdPath;
details.triggers = extractTriggers(content);
details.content = content;
}
// Check manifest for scripts
const manifestPaths = deduplicateManifests([
...findManifests(path.join(agenticDir, 'frameworks')),
...findManifests(path.join(agenticDir, 'addons')),
]);
for (const manifestPath of manifestPaths) {
try {
const raw = fs.readFileSync(manifestPath, 'utf-8');
const manifest = JSON.parse(raw);
const skillEntry = manifest.skills.find((s) => s.name === name);
if (skillEntry) {
details.triggers = details.triggers?.length
? details.triggers
: skillEntry.triggers;
details.scripts = skillEntry.scripts;
details.version = manifest.version;
break;
}
}
catch {
// Skip
}
}
return details;
}
async install(name, options) {
const details = await this.info(name);
if (!details || !details.path) {
throw new Error(`Skill '${name}' not found in local registry`);
}
const target = options.target || 'claude';
const projectDir = options.projectDir;
// Use PlatformSkillResolver for cross-platform path resolution
let targetDir;
try {
const { PlatformSkillResolver } = await import('../../smiths/skillsmith/platform-resolver.js');
targetDir = PlatformSkillResolver.getSkillPath(target, projectDir, name);
}
catch {
// Fallback: construct path from known conventions
const platformDirs = {
claude: '.claude/skills',
copilot: '.github/skills',
factory: '.factory/skills',
cursor: '.cursor/skills',
codex: '.codex/skills',
opencode: '.opencode/skill',
warp: '.warp/skills',
windsurf: '.windsurf/skills',
openclaw: path.join(process.env.HOME || '~', '.openclaw', 'skills'),
generic: 'skills',
};
const baseDir = platformDirs[target] || `.${target}/skills`;
targetDir = path.join(projectDir, baseDir, name);
}
// Copy skill directory to target
const sourceDir = path.dirname(details.path);
fs.mkdirSync(targetDir, { recursive: true });
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(sourceDir, entry.name);
const destPath = path.join(targetDir, entry.name);
if (entry.isFile()) {
fs.copyFileSync(srcPath, destPath);
}
else if (entry.isDirectory()) {
fs.cpSync(srcPath, destPath, { recursive: true });
}
}
// Codex `agents/openai.yaml` UI sidecar (#1129 PUW-028).
// Additive — sidecar absence is graceful. Failures here are non-fatal.
if (target === 'codex') {
try {
const { emitCodexSidecar } = await import('../../smiths/skillsmith/codex-sidecar.js');
const skillMdPath = path.join(targetDir, 'SKILL.md');
let metadata = {};
try {
const skillContent = fs.readFileSync(skillMdPath, 'utf8');
const fmMatch = /^---\s*\n([\s\S]*?)\n---/.exec(skillContent);
if (fmMatch) {
const yaml = await import('js-yaml');
const fm = yaml.load(fmMatch[1]);
if (fm && typeof fm === 'object') {
metadata = fm;
}
}
}
catch {
// Frontmatter parse failure → fall through with empty metadata;
// sidecar will use folder name as display_name.
}
await emitCodexSidecar(targetDir, metadata);
}
catch (err) {
// Sidecar emission must never block the deploy. Log and move on.
console.warn(`Warning: Codex sidecar emission failed for ${name}: ${err instanceof Error ? err.message : String(err)}`);
}
}
}
/**
* Find all SKILL.md files recursively
*/
findSkillMds(dir) {
const results = [];
if (!fs.existsSync(dir))
return results;
const walk = (currentDir) => {
try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
}
else if (entry.name === 'SKILL.md') {
results.push(fullPath);
}
}
}
catch {
// Permission error or similar — skip
}
};
walk(dir);
return results;
}
}
//# sourceMappingURL=local.js.map