156 lines
4.2 KiB
JavaScript
156 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { resolveProjectPath } = require('./path-safety');
|
|
|
|
const KNOWN_INSTRUCTION_PATHS = [
|
|
'AGENTS.md',
|
|
'CLAUDE.md',
|
|
'GEMINI.md',
|
|
'.cursorrules',
|
|
'.windsurfrules',
|
|
'.github/copilot-instructions.md',
|
|
];
|
|
|
|
function normalizeSkillName(value) {
|
|
const normalized = String(value || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
if (!normalized) {
|
|
throw new Error('skillName is required.');
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function statFile(filePath) {
|
|
try {
|
|
return fs.statSync(filePath);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function listSkillFiles(projectPath) {
|
|
const skillRoot = resolveProjectPath(projectPath, '.codex/skills');
|
|
if (!fs.existsSync(skillRoot)) {
|
|
return [];
|
|
}
|
|
|
|
const skills = [];
|
|
const stack = [skillRoot];
|
|
while (stack.length) {
|
|
const current = stack.pop();
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
const fullPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath);
|
|
continue;
|
|
}
|
|
if (entry.name === 'SKILL.md') {
|
|
const stat = statFile(fullPath);
|
|
skills.push({
|
|
path: path.relative(projectPath, fullPath).replace(/\\/g, '/'),
|
|
size: stat ? stat.size : 0,
|
|
mtime: stat ? stat.mtime.toISOString() : '',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return skills.sort((left, right) => left.path.localeCompare(right.path));
|
|
}
|
|
|
|
function listProjectInstructions(projectPath) {
|
|
const files = [];
|
|
for (const relativePath of KNOWN_INSTRUCTION_PATHS) {
|
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
|
const stat = statFile(fullPath);
|
|
if (stat && stat.isFile()) {
|
|
files.push({
|
|
path: relativePath,
|
|
size: stat.size,
|
|
mtime: stat.mtime.toISOString(),
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
files,
|
|
skills: listSkillFiles(projectPath),
|
|
};
|
|
}
|
|
|
|
function readProjectInstruction(projectPath, target) {
|
|
const relativePath = String(target || '').trim();
|
|
if (!relativePath) {
|
|
throw new Error('target is required.');
|
|
}
|
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
|
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
|
|
throw new Error(`Instruction file not found: ${relativePath}`);
|
|
}
|
|
return {
|
|
path: relativePath,
|
|
content: fs.readFileSync(fullPath, 'utf8'),
|
|
};
|
|
}
|
|
|
|
function writeProjectInstruction(projectPath, options = {}) {
|
|
const relativePath = String(options.target || '').trim();
|
|
if (!relativePath) {
|
|
throw new Error('target is required.');
|
|
}
|
|
const content = String(options.content || '');
|
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
|
if (fs.existsSync(fullPath) && options.overwrite === false) {
|
|
throw new Error(`Instruction file already exists: ${relativePath}`);
|
|
}
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
fs.writeFileSync(fullPath, content, 'utf8');
|
|
const stat = fs.statSync(fullPath);
|
|
return {
|
|
written: true,
|
|
path: relativePath,
|
|
size: stat.size,
|
|
mtime: stat.mtime.toISOString(),
|
|
};
|
|
}
|
|
|
|
function createProjectSkill(projectPath, options = {}) {
|
|
const skillName = normalizeSkillName(options.skillName);
|
|
const title = String(options.title || skillName).trim();
|
|
const description = String(options.description || `Project-specific workflow for ${title}.`).trim();
|
|
const body = String(options.instructions || '').trim() || [
|
|
`Use this skill for ${title} work in this Cocos project.`,
|
|
'',
|
|
'- Inspect the active scene and project context before editing.',
|
|
'- Prefer focused MCP tools before broad manual file edits.',
|
|
'- Run relevant validation tools after changes.',
|
|
].join('\n');
|
|
const relativePath = `.codex/skills/${skillName}/SKILL.md`;
|
|
const content = [
|
|
`# ${title}`,
|
|
'',
|
|
`Description: ${description}`,
|
|
'',
|
|
'## Instructions',
|
|
body,
|
|
'',
|
|
].join('\n');
|
|
return writeProjectInstruction(projectPath, {
|
|
target: relativePath,
|
|
content,
|
|
overwrite: options.overwrite !== false,
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
KNOWN_INSTRUCTION_PATHS,
|
|
createProjectSkill,
|
|
listProjectInstructions,
|
|
readProjectInstruction,
|
|
writeProjectInstruction,
|
|
};
|