Release v0.3.0
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
'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,
|
||||
};
|
||||
Reference in New Issue
Block a user