UNPKG

i18ntk

Version:

i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.

123 lines (107 loc) 5.53 kB
const fs = require('fs'); const os = require('os'); const path = require('path'); const SecurityUtils = require('./security'); const SKILL_NAME = 'i18ntk'; const SOURCE_DIR = path.join(__dirname, '..', 'skills', SKILL_NAME); function pathExists(value) { try { fs.accessSync(path.resolve(value), fs.constants.F_OK); return true; } catch (_) { return false; } } function normalizeAgents(value) { if (!value) return null; const items = Array.isArray(value) ? value : String(value).split(/[;,]/); return new Set(items.map(item => String(item).trim().toLowerCase()).filter(Boolean)); } function discoverSkillTargets(options = {}) { const home = path.resolve(options.homeDir || os.homedir()); const project = path.resolve(options.projectRoot || process.cwd()); const env = options.env || process.env; const includePotential = options.includePotential === true; const agentFilter = normalizeAgents(options.agents); const scopeFilter = options.scope && options.scope !== 'all' ? String(options.scope).toLowerCase() : null; const candidates = []; function add(id, agent, label, scope, root, explicit = false) { if (agentFilter && !agentFilter.has(agent)) return; if (scopeFilter && scope !== scopeFilter) return; const resolvedRoot = path.resolve(root); const parentDetected = pathExists(resolvedRoot) || pathExists(path.dirname(resolvedRoot)); const detected = explicit || parentDetected; if (!detected && !includePotential && !agentFilter) return; candidates.push({ id, agent, label, scope, root: resolvedRoot, detected }); } const codexHome = path.resolve(env.CODEX_HOME || path.join(home, '.codex')); const claudeHome = path.resolve(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude')); add('codex-personal', 'codex', 'OpenAI Codex (personal)', 'personal', path.join(codexHome, 'skills'), Boolean(env.CODEX_HOME)); add('claude-personal', 'claude', 'Claude Code (personal)', 'personal', path.join(claudeHome, 'skills'), Boolean(env.CLAUDE_CONFIG_DIR)); add('copilot-personal', 'copilot', 'GitHub Copilot (personal)', 'personal', path.join(home, '.copilot', 'skills')); add('agents-personal', 'agents', 'Shared Agent Skills (personal)', 'personal', path.join(home, '.agents', 'skills')); add('codex-project', 'codex', 'OpenAI Codex (project)', 'project', path.join(project, '.codex', 'skills')); add('claude-project', 'claude', 'Claude Code (project)', 'project', path.join(project, '.claude', 'skills')); add('copilot-project', 'copilot', 'GitHub Copilot (project)', 'project', path.join(project, '.github', 'skills')); add('agents-project', 'agents', 'Shared Agent Skills (project)', 'project', path.join(project, '.agents', 'skills')); if (env.COPILOT_SKILLS_DIRS) { String(env.COPILOT_SKILLS_DIRS).split(/[;,]/).map(value => value.trim()).filter(Boolean) .forEach((root, index) => add(`copilot-custom-${index + 1}`, 'copilot', 'GitHub Copilot (custom)', 'personal', root, true)); } const seen = new Set(); return candidates.filter(candidate => { const key = process.platform === 'win32' ? candidate.root.toLowerCase() : candidate.root; if (seen.has(key)) return false; seen.add(key); return true; }); } function collectSourceFiles(dir = SOURCE_DIR, relative = '') { const results = []; const entries = SecurityUtils.safeReaddirSync(dir, SOURCE_DIR, { withFileTypes: true }) || []; for (const entry of entries) { const childRelative = relative ? path.join(relative, entry.name) : entry.name; const fullPath = path.join(dir, entry.name); if (entry.isSymbolicLink()) continue; if (entry.isDirectory()) results.push(...collectSourceFiles(fullPath, childRelative)); else if (entry.isFile()) results.push({ fullPath, relativePath: childRelative }); } return results; } function installSkillTarget(target, options = {}) { if (!target || typeof target.root !== 'string') throw new Error('Invalid skill installation target.'); if (!pathExists(path.join(SOURCE_DIR, 'SKILL.md'))) throw new Error('Bundled i18ntk skill is missing.'); const root = path.resolve(target.root); const destination = path.join(root, SKILL_NAME); const exists = pathExists(path.join(destination, 'SKILL.md')); if (exists && options.force !== true) { return { ...target, destination, status: 'skipped', reason: 'exists' }; } if (options.dryRun === true) { return { ...target, destination, status: exists ? 'would-update' : 'would-install' }; } fs.mkdirSync(root, { recursive: true }); const sourceFiles = collectSourceFiles().filter(file => { if (target.agent === 'codex') return true; return file.relativePath.split(path.sep)[0] !== 'agents'; }); for (const file of sourceFiles) { const destinationFile = path.join(destination, file.relativePath); const content = SecurityUtils.safeReadFileSync(file.fullPath, SOURCE_DIR, 'utf8'); if (content === null) throw new Error(`Unable to read bundled skill file: ${file.relativePath}`); if (!SecurityUtils.safeWriteFileSync(destinationFile, content, root)) { throw new Error(`Unable to install skill file: ${file.relativePath}`); } } return { ...target, destination, status: exists ? 'updated' : 'installed', files: sourceFiles.length }; } function installSkillTargets(targets, options = {}) { return targets.map(target => installSkillTarget(target, options)); } module.exports = { SKILL_NAME, SOURCE_DIR, discoverSkillTargets, installSkillTarget, installSkillTargets, };