mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 21:53:28 +08:00
188 lines
4.7 KiB
JavaScript
188 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const { ensureDir } = require('../utils');
|
|
|
|
const PROVENANCE_FILE_NAME = '.provenance.json';
|
|
const SKILL_TYPES = Object.freeze({
|
|
CURATED: 'curated',
|
|
LEARNED: 'learned',
|
|
IMPORTED: 'imported',
|
|
UNKNOWN: 'unknown',
|
|
});
|
|
|
|
function resolveRepoRoot(repoRoot) {
|
|
if (repoRoot) {
|
|
return path.resolve(repoRoot);
|
|
}
|
|
|
|
return path.resolve(__dirname, '..', '..', '..');
|
|
}
|
|
|
|
function resolveHomeDir(homeDir) {
|
|
return homeDir ? path.resolve(homeDir) : os.homedir();
|
|
}
|
|
|
|
function normalizeSkillDir(skillPath) {
|
|
if (!skillPath || typeof skillPath !== 'string') {
|
|
throw new Error('skillPath is required');
|
|
}
|
|
|
|
const resolvedPath = path.resolve(skillPath);
|
|
if (path.basename(resolvedPath) === 'SKILL.md') {
|
|
return path.dirname(resolvedPath);
|
|
}
|
|
|
|
return resolvedPath;
|
|
}
|
|
|
|
function isWithinRoot(targetPath, rootPath) {
|
|
const relativePath = path.relative(rootPath, targetPath);
|
|
return relativePath === '' || (
|
|
!relativePath.startsWith('..')
|
|
&& !path.isAbsolute(relativePath)
|
|
);
|
|
}
|
|
|
|
function getSkillRoots(options = {}) {
|
|
const repoRoot = resolveRepoRoot(options.repoRoot);
|
|
const homeDir = resolveHomeDir(options.homeDir);
|
|
|
|
return {
|
|
curated: path.join(repoRoot, 'skills'),
|
|
learned: path.join(homeDir, '.claude', 'skills', 'learned'),
|
|
imported: path.join(homeDir, '.claude', 'skills', 'imported'),
|
|
};
|
|
}
|
|
|
|
function classifySkillPath(skillPath, options = {}) {
|
|
const skillDir = normalizeSkillDir(skillPath);
|
|
const roots = getSkillRoots(options);
|
|
|
|
if (isWithinRoot(skillDir, roots.curated)) {
|
|
return SKILL_TYPES.CURATED;
|
|
}
|
|
|
|
if (isWithinRoot(skillDir, roots.learned)) {
|
|
return SKILL_TYPES.LEARNED;
|
|
}
|
|
|
|
if (isWithinRoot(skillDir, roots.imported)) {
|
|
return SKILL_TYPES.IMPORTED;
|
|
}
|
|
|
|
return SKILL_TYPES.UNKNOWN;
|
|
}
|
|
|
|
function requiresProvenance(skillPath, options = {}) {
|
|
const skillType = classifySkillPath(skillPath, options);
|
|
return skillType === SKILL_TYPES.LEARNED || skillType === SKILL_TYPES.IMPORTED;
|
|
}
|
|
|
|
function getProvenancePath(skillPath) {
|
|
return path.join(normalizeSkillDir(skillPath), PROVENANCE_FILE_NAME);
|
|
}
|
|
|
|
function isIsoTimestamp(value) {
|
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const timestamp = Date.parse(value);
|
|
return !Number.isNaN(timestamp);
|
|
}
|
|
|
|
function validateProvenance(record) {
|
|
const errors = [];
|
|
|
|
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
errors.push('provenance record must be an object');
|
|
return {
|
|
valid: false,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
if (typeof record.source !== 'string' || record.source.trim().length === 0) {
|
|
errors.push('source is required');
|
|
}
|
|
|
|
if (!isIsoTimestamp(record.created_at)) {
|
|
errors.push('created_at must be an ISO timestamp');
|
|
}
|
|
|
|
if (typeof record.confidence !== 'number' || Number.isNaN(record.confidence)) {
|
|
errors.push('confidence must be a number');
|
|
} else if (record.confidence < 0 || record.confidence > 1) {
|
|
errors.push('confidence must be between 0 and 1');
|
|
}
|
|
|
|
if (typeof record.author !== 'string' || record.author.trim().length === 0) {
|
|
errors.push('author is required');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
function assertValidProvenance(record) {
|
|
const validation = validateProvenance(record);
|
|
if (!validation.valid) {
|
|
throw new Error(`Invalid provenance metadata: ${validation.errors.join('; ')}`);
|
|
}
|
|
}
|
|
|
|
function readProvenance(skillPath, options = {}) {
|
|
const skillDir = normalizeSkillDir(skillPath);
|
|
const provenancePath = getProvenancePath(skillDir);
|
|
const provenanceRequired = options.required === true || requiresProvenance(skillDir, options);
|
|
|
|
if (!fs.existsSync(provenancePath)) {
|
|
if (provenanceRequired) {
|
|
throw new Error(`Missing provenance metadata for ${skillDir}`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const record = JSON.parse(fs.readFileSync(provenancePath, 'utf8'));
|
|
assertValidProvenance(record);
|
|
return record;
|
|
}
|
|
|
|
function writeProvenance(skillPath, record, options = {}) {
|
|
const skillDir = normalizeSkillDir(skillPath);
|
|
|
|
if (!requiresProvenance(skillDir, options)) {
|
|
throw new Error(`Provenance metadata is only required for learned or imported skills: ${skillDir}`);
|
|
}
|
|
|
|
assertValidProvenance(record);
|
|
|
|
const provenancePath = getProvenancePath(skillDir);
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(provenancePath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
|
|
return {
|
|
path: provenancePath,
|
|
record: { ...record },
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
PROVENANCE_FILE_NAME,
|
|
SKILL_TYPES,
|
|
classifySkillPath,
|
|
getProvenancePath,
|
|
getSkillRoots,
|
|
readProvenance,
|
|
requiresProvenance,
|
|
validateProvenance,
|
|
writeProvenance,
|
|
};
|