agents-opencode
Advanced tools
| 'use strict'; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { readJsonFile, writeJsonFile, isObject } = require('./file-ops.js'); | ||
| function mergeInstallerConfig(targetConfigPath, sourceConfig, onBeforeWrite, logWarning) { | ||
| const patch = { | ||
| createdFile: false, | ||
| addedPermissionKeys: [], | ||
| addedPluginEntries: [], | ||
| createdSchema: false, | ||
| skipped: false, | ||
| changed: false, | ||
| }; | ||
| const sourceConfigForInstall = Object.assign({}, (sourceConfig || {})); | ||
| delete sourceConfigForInstall.instructions; | ||
| if (!fs.existsSync(targetConfigPath)) { | ||
| writeJsonFile(targetConfigPath, sourceConfigForInstall); | ||
| patch.createdFile = true; | ||
| patch.changed = true; | ||
| if (isObject(sourceConfigForInstall.permission)) { | ||
| patch.addedPermissionKeys = Object.keys(sourceConfigForInstall.permission); | ||
| } | ||
| if (Array.isArray(sourceConfigForInstall.plugin)) { | ||
| patch.addedPluginEntries = sourceConfigForInstall.plugin.slice(); | ||
| } | ||
| patch.createdSchema = !!sourceConfigForInstall.$schema; | ||
| return patch; | ||
| } | ||
| const existing = readJsonFile(targetConfigPath, `existing config at ${targetConfigPath}`, logWarning); | ||
| if (!existing || !isObject(existing)) { | ||
| if (logWarning) { | ||
| logWarning(`Skipping config merge for ${targetConfigPath} because existing config is invalid JSON.`); | ||
| } | ||
| patch.skipped = true; | ||
| return patch; | ||
| } | ||
| if (isObject(sourceConfigForInstall.permission)) { | ||
| if (existing.permission === undefined) { | ||
| existing.permission = {}; | ||
| } | ||
| if (!isObject(existing.permission)) { | ||
| if (logWarning) { | ||
| logWarning(`Skipping permission merge for ${targetConfigPath}; existing permission is not an object.`); | ||
| } | ||
| } else { | ||
| for (const key of Object.keys(sourceConfigForInstall.permission)) { | ||
| if (!(key in existing.permission)) { | ||
| existing.permission[key] = sourceConfigForInstall.permission[key]; | ||
| patch.addedPermissionKeys.push(key); | ||
| patch.changed = true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (Array.isArray(sourceConfigForInstall.plugin)) { | ||
| if (!Array.isArray(existing.plugin)) { | ||
| existing.plugin = []; | ||
| } | ||
| for (const entry of sourceConfigForInstall.plugin) { | ||
| if (!existing.plugin.includes(entry)) { | ||
| existing.plugin.push(entry); | ||
| patch.addedPluginEntries.push(entry); | ||
| patch.changed = true; | ||
| } | ||
| } | ||
| } | ||
| if (patch.changed) { | ||
| if (typeof onBeforeWrite === 'function') { | ||
| onBeforeWrite(); | ||
| } | ||
| writeJsonFile(targetConfigPath, existing); | ||
| } | ||
| return patch; | ||
| } | ||
| function mergeConfigPatches(existingPatch, currentPatch) { | ||
| const base = { | ||
| createdFile: false, | ||
| addedPermissionKeys: [], | ||
| createdSchema: false, | ||
| skipped: false, | ||
| changed: false, | ||
| }; | ||
| const prior = isObject(existingPatch) ? existingPatch : {}; | ||
| const next = isObject(currentPatch) ? currentPatch : {}; | ||
| const permissionKeys = new Set([ | ||
| ...(Array.isArray(prior.addedPermissionKeys) ? prior.addedPermissionKeys : []), | ||
| ...(Array.isArray(next.addedPermissionKeys) ? next.addedPermissionKeys : []), | ||
| ]); | ||
| return { | ||
| ...base, | ||
| ...prior, | ||
| ...next, | ||
| createdFile: Boolean(prior.createdFile || next.createdFile), | ||
| addedPermissionKeys: [...permissionKeys], | ||
| createdSchema: Boolean(prior.createdSchema || next.createdSchema || prior.addedSchema || next.addedSchema), | ||
| skipped: Boolean(prior.skipped || next.skipped), | ||
| changed: Boolean(prior.changed || next.changed), | ||
| }; | ||
| } | ||
| function loadSourceConfig(sourceDir, logWarning) { | ||
| const sourceConfigPath = path.join(sourceDir, 'opencode.json'); | ||
| const sourceConfig = readJsonFile(sourceConfigPath, 'package opencode.json', logWarning); | ||
| if (!sourceConfig || !isObject(sourceConfig)) { | ||
| throw new Error('Could not parse package opencode.json.'); | ||
| } | ||
| return sourceConfig; | ||
| } | ||
| function configLooksManaged(configPath, sourceConfig, logWarning) { | ||
| if (!fs.existsSync(configPath)) { | ||
| return false; | ||
| } | ||
| const config = readJsonFile(configPath, `config at ${configPath}`, logWarning); | ||
| if (!config || !isObject(config)) { | ||
| return false; | ||
| } | ||
| if (isObject(sourceConfig.permission) && isObject(config.permission)) { | ||
| for (const key of Object.keys(sourceConfig.permission)) { | ||
| if (!(key in config.permission)) { | ||
| continue; | ||
| } | ||
| if (JSON.stringify(config.permission[key]) === JSON.stringify(sourceConfig.permission[key])) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| function manifestlessCleanup(configPath, sourceConfig, onBeforeMutate, logWarning) { | ||
| if (!fs.existsSync(configPath)) { | ||
| return { changed: false, removedFile: false }; | ||
| } | ||
| const existing = readJsonFile(configPath, `existing config at ${configPath}`, logWarning); | ||
| if (!existing || !isObject(existing)) { | ||
| if (logWarning) { | ||
| logWarning(`Skipping manifestless config cleanup; invalid JSON at ${configPath}.`); | ||
| } | ||
| return { changed: false, removedFile: false }; | ||
| } | ||
| if (JSON.stringify(existing) === JSON.stringify(sourceConfig)) { | ||
| if (typeof onBeforeMutate === 'function') { | ||
| onBeforeMutate(); | ||
| } | ||
| fs.unlinkSync(configPath); | ||
| return { changed: true, removedFile: true }; | ||
| } | ||
| let changed = false; | ||
| if (isObject(sourceConfig.permission) && isObject(existing.permission)) { | ||
| for (const key of Object.keys(sourceConfig.permission)) { | ||
| if (!(key in existing.permission)) { | ||
| continue; | ||
| } | ||
| if (JSON.stringify(existing.permission[key]) === JSON.stringify(sourceConfig.permission[key])) { | ||
| delete existing.permission[key]; | ||
| changed = true; | ||
| } | ||
| } | ||
| if (Object.keys(existing.permission).length === 0) { | ||
| delete existing.permission; | ||
| changed = true; | ||
| } | ||
| } | ||
| if (changed) { | ||
| if (typeof onBeforeMutate === 'function') { | ||
| onBeforeMutate(); | ||
| } | ||
| writeJsonFile(configPath, existing); | ||
| } | ||
| return { changed, removedFile: false }; | ||
| } | ||
| function checkLegacyAgentDir(opencodeDir, opts) { | ||
| const logError = opts.error; | ||
| const pkgName = opts.PACKAGE_NAME; | ||
| const AGENT_DIR_LEGACY = opts.AGENT_DIR_LEGACY; | ||
| const AGENT_DIR = opts.AGENT_DIR; | ||
| const legacyDir = path.join(opencodeDir, AGENT_DIR_LEGACY); | ||
| if (fs.existsSync(legacyDir)) { | ||
| if (logError) { | ||
| logError(`Legacy '.opencode/${AGENT_DIR_LEGACY}/' directory detected.`); | ||
| logError(`v2.0.0+ only supports '.opencode/${AGENT_DIR}/' (plural).`); | ||
| logError(''); | ||
| logError(`Migration: mv ${legacyDir} ${path.join(opencodeDir, AGENT_DIR)}`); | ||
| logError(`Then re-run: npx ${pkgName}`); | ||
| } | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| module.exports = { | ||
| mergeInstallerConfig, | ||
| mergeConfigPatches, | ||
| loadSourceConfig, | ||
| configLooksManaged, | ||
| manifestlessCleanup, | ||
| checkLegacyAgentDir, | ||
| }; |
| 'use strict'; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { toManagedPath } = require('./paths.js'); | ||
| const BACKUP_DIR = '.backups'; | ||
| const LANGUAGE_MAP = { | ||
| dotnet: 'dotnet-clean-architecture.instructions.md', | ||
| python: 'python-best-practices.instructions.md', | ||
| typescript: 'typescript-strict.instructions.md', | ||
| flutter: 'flutter.instructions.md', | ||
| go: 'go.instructions.md', | ||
| java: 'java-spring-boot.instructions.md', | ||
| node: 'node-express.instructions.md', | ||
| react: 'react-next.instructions.md', | ||
| ruby: 'ruby-on-rails.instructions.md', | ||
| rust: 'rust.instructions.md', | ||
| sql: 'sql-migrations.instructions.md', | ||
| cicd: 'ci-cd-hygiene.instructions.md', | ||
| }; | ||
| const LANGUAGE_INSTRUCTIONS = new Set(Object.values(LANGUAGE_MAP)); | ||
| const ALWAYS_KEEP = [ | ||
| 'blogger.instructions.md', | ||
| 'brutal-critic.instructions.md', | ||
| 'ci-cd-hygiene.instructions.md', | ||
| ]; | ||
| function isObject(value) { | ||
| return value !== null && typeof value === 'object' && !Array.isArray(value); | ||
| } | ||
| function ensureDir(dirPath) { | ||
| if (!fs.existsSync(dirPath)) { | ||
| fs.mkdirSync(dirPath, { recursive: true }); | ||
| } | ||
| } | ||
| function readJsonFile(filePath, labelForError, logWarning) { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(filePath, 'utf8')); | ||
| } catch (err) { | ||
| if (logWarning) { | ||
| logWarning(`Could not parse ${labelForError}: ${err.message}`); | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| function writeJsonFile(filePath, data) { | ||
| fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); | ||
| } | ||
| function removeIfExists(filePath) { | ||
| if (!fs.existsSync(filePath)) { | ||
| return false; | ||
| } | ||
| fs.unlinkSync(filePath); | ||
| return true; | ||
| } | ||
| function removeManagedFile(absolutePath, relativePathFromRoot, paths, backupSession) { | ||
| if (!fs.existsSync(absolutePath)) { | ||
| return { removed: false, directory: null }; | ||
| } | ||
| backupSession.backupFile(absolutePath, relativePathFromRoot || path.relative(paths.rootDir, absolutePath)); | ||
| fs.unlinkSync(absolutePath); | ||
| return { removed: true, directory: path.dirname(absolutePath) }; | ||
| } | ||
| function listFilesRecursive(rootDir) { | ||
| const files = []; | ||
| function walk(currentDir, relativeBase) { | ||
| const entries = fs.readdirSync(currentDir, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| const relativePath = relativeBase ? path.join(relativeBase, entry.name) : entry.name; | ||
| const absolutePath = path.join(currentDir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| walk(absolutePath, relativePath); | ||
| } else if (entry.isFile()) { | ||
| files.push(relativePath); | ||
| } | ||
| } | ||
| } | ||
| walk(rootDir, ''); | ||
| return files; | ||
| } | ||
| function getManagedSourceFiles(sourceOpencodeDir) { | ||
| const allFiles = listFilesRecursive(sourceOpencodeDir); | ||
| return allFiles.filter((relativePath) => { | ||
| if (relativePath.includes(`node_modules${path.sep}`) || relativePath === 'node_modules') { | ||
| return false; | ||
| } | ||
| if (relativePath === BACKUP_DIR || relativePath.startsWith(`${BACKUP_DIR}${path.sep}`)) { | ||
| return false; | ||
| } | ||
| if (/\.backup\./.test(relativePath)) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| } | ||
| function filesEqual(pathA, pathB) { | ||
| try { | ||
| const statA = fs.statSync(pathA); | ||
| const statB = fs.statSync(pathB); | ||
| if (statA.size !== statB.size) { | ||
| return false; | ||
| } | ||
| const contentA = fs.readFileSync(pathA); | ||
| const contentB = fs.readFileSync(pathB); | ||
| return contentA.equals(contentB); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function buildManagedFilesFromSource(scope, sourceFiles, paths) { | ||
| const managedFiles = []; | ||
| for (const relativeFile of sourceFiles) { | ||
| const managedPath = toManagedPath(scope, relativeFile); | ||
| const absoluteManagedPath = path.join(paths.rootDir, managedPath); | ||
| if (fs.existsSync(absoluteManagedPath)) { | ||
| managedFiles.push(managedPath); | ||
| } | ||
| } | ||
| return managedFiles; | ||
| } | ||
| function installManagedTree(sourceOpencodeDir, sourceFiles, destinationOpencodeDir, scope, backupSession, logWarning) { | ||
| ensureDir(destinationOpencodeDir); | ||
| let copiedCount = 0; | ||
| let skippedCount = 0; | ||
| let backupCount = 0; | ||
| for (const relativeFile of sourceFiles) { | ||
| const src = path.join(sourceOpencodeDir, relativeFile); | ||
| const dest = path.join(destinationOpencodeDir, relativeFile); | ||
| const destParent = path.dirname(dest); | ||
| ensureDir(destParent); | ||
| if (fs.existsSync(dest)) { | ||
| if (filesEqual(src, dest)) { | ||
| skippedCount += 1; | ||
| continue; | ||
| } | ||
| try { | ||
| if (backupSession && backupSession.backupFile(dest, toManagedPath(scope, relativeFile))) { | ||
| backupCount += 1; | ||
| } | ||
| } catch (err) { | ||
| if (logWarning) { | ||
| logWarning(`Could not back up existing file ${dest}: ${err.message}`); | ||
| } | ||
| } | ||
| } | ||
| fs.copyFileSync(src, dest); | ||
| copiedCount += 1; | ||
| } | ||
| return { | ||
| copiedCount, | ||
| skippedCount, | ||
| backupCount, | ||
| }; | ||
| } | ||
| function filterLanguages(installDir, languages, logFns) { | ||
| const logWarning = logFns && logFns.warning; | ||
| const logInfo = logFns && logFns.info; | ||
| const logSuccess = logFns && logFns.success; | ||
| const instructionsDir = path.join(installDir, 'instructions'); | ||
| if (!fs.existsSync(instructionsDir)) { | ||
| if (logWarning) logWarning('No instructions directory found — skipping language filter.'); | ||
| return; | ||
| } | ||
| const validLanguages = Object.keys(LANGUAGE_MAP); | ||
| const requested = languages.split(',').map(function (l) { return l.trim().toLowerCase(); }).filter(Boolean); | ||
| const invalid = requested.filter(function (l) { return !validLanguages.includes(l); }); | ||
| if (invalid.length > 0) { | ||
| if (logWarning) logWarning(`Unknown language(s): ${invalid.join(', ')}`); | ||
| if (logInfo) logInfo(`Available: ${validLanguages.join(', ')}`); | ||
| } | ||
| const valid = requested.filter(function (l) { return validLanguages.includes(l); }); | ||
| if (valid.length === 0) { | ||
| if (logWarning) logWarning('No valid languages specified — keeping all instruction files.'); | ||
| return; | ||
| } | ||
| const keepFiles = new Set(ALWAYS_KEEP); | ||
| for (var i = 0; i < valid.length; i++) { | ||
| if (LANGUAGE_MAP[valid[i]]) { | ||
| keepFiles.add(LANGUAGE_MAP[valid[i]]); | ||
| } | ||
| } | ||
| const allFiles = fs.readdirSync(instructionsDir); | ||
| const removed = []; | ||
| for (var j = 0; j < allFiles.length; j++) { | ||
| var file = allFiles[j]; | ||
| if (keepFiles.has(file) || !LANGUAGE_INSTRUCTIONS.has(file)) { | ||
| continue; | ||
| } | ||
| try { | ||
| fs.unlinkSync(path.join(instructionsDir, file)); | ||
| removed.push(file); | ||
| } catch (err) { | ||
| if (logWarning) logWarning(`Could not remove ${file}: ${err.message}`); | ||
| } | ||
| } | ||
| if (logSuccess) logSuccess(`✓ Applied language filter: ${valid.join(', ')}`); | ||
| if (removed.length > 0) { | ||
| if (logInfo) logInfo(`Removed ${removed.length} instruction file(s): ${removed.join(', ')}`); | ||
| } | ||
| } | ||
| function pruneEmptyDirectories(directories, stopAtDirectory) { | ||
| const sorted = Array.from(directories).sort(function (a, b) { return b.length - a.length; }); | ||
| let prunedCount = 0; | ||
| for (var i = 0; i < sorted.length; i++) { | ||
| var dirPath = sorted[i]; | ||
| if (!fs.existsSync(dirPath)) { | ||
| continue; | ||
| } | ||
| if (path.resolve(dirPath) === path.resolve(stopAtDirectory)) { | ||
| continue; | ||
| } | ||
| try { | ||
| const entries = fs.readdirSync(dirPath); | ||
| if (entries.length === 0) { | ||
| fs.rmdirSync(dirPath); | ||
| prunedCount += 1; | ||
| } | ||
| } catch { | ||
| // ignore pruning errors | ||
| } | ||
| } | ||
| return prunedCount; | ||
| } | ||
| module.exports = { | ||
| isObject, | ||
| ensureDir, | ||
| readJsonFile, | ||
| writeJsonFile, | ||
| removeIfExists, | ||
| removeManagedFile, | ||
| listFilesRecursive, | ||
| getManagedSourceFiles, | ||
| filesEqual, | ||
| buildManagedFilesFromSource, | ||
| installManagedTree, | ||
| filterLanguages, | ||
| pruneEmptyDirectories, | ||
| }; |
| 'use strict'; | ||
| const os = require('os'); | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
| const MANIFEST_FILE = '.agents-opencode-manifest.json'; | ||
| const VERSION_FILE = '.opencode-agents-version'; | ||
| const AGENT_DIR = 'agents'; | ||
| function getHomeDir() { | ||
| return os.homedir(); | ||
| } | ||
| function getGlobalConfigDir() { | ||
| return path.join(getHomeDir(), '.config', 'opencode'); | ||
| } | ||
| function getScopePaths(scope, projectDir) { | ||
| if (scope === 'global') { | ||
| const rootDir = getGlobalConfigDir(); | ||
| return { | ||
| scope, | ||
| rootDir, | ||
| opencodeDir: rootDir, | ||
| manifestPath: path.join(rootDir, MANIFEST_FILE), | ||
| versionPath: path.join(rootDir, VERSION_FILE), | ||
| configPath: path.join(rootDir, 'opencode.json'), | ||
| agentsMdPath: null, | ||
| }; | ||
| } | ||
| const resolvedProjectDir = path.resolve(projectDir || process.cwd()); | ||
| return { | ||
| scope, | ||
| rootDir: resolvedProjectDir, | ||
| opencodeDir: path.join(resolvedProjectDir, '.opencode'), | ||
| manifestPath: path.join(resolvedProjectDir, '.opencode', MANIFEST_FILE), | ||
| versionPath: path.join(resolvedProjectDir, VERSION_FILE), | ||
| configPath: path.join(resolvedProjectDir, 'opencode.json'), | ||
| agentsMdPath: path.join(resolvedProjectDir, 'AGENTS.md'), | ||
| }; | ||
| } | ||
| function toManagedPath(scope, relativeOpencodeFile) { | ||
| if (scope === 'global') { | ||
| return relativeOpencodeFile; | ||
| } | ||
| return path.join('.opencode', relativeOpencodeFile); | ||
| } | ||
| function resolveAgentDirectory(opencodeDir) { | ||
| const agentDir = path.join(opencodeDir, AGENT_DIR); | ||
| if (fs.existsSync(agentDir)) { | ||
| return agentDir; | ||
| } | ||
| return null; | ||
| } | ||
| module.exports = { | ||
| MANIFEST_FILE, | ||
| VERSION_FILE, | ||
| AGENT_DIR, | ||
| getHomeDir, | ||
| getGlobalConfigDir, | ||
| getScopePaths, | ||
| toManagedPath, | ||
| resolveAgentDirectory, | ||
| }; |
+4
-2
| { | ||
| "name": "agents-opencode", | ||
| "version": "2.0.0", | ||
| "version": "2.0.1", | ||
| "description": "OpenCode Agents: Intelligent AI assistants for software development. Features 9 specialized agents (including legal-advisor for license auditing and compliance), 14 coding standards, automated code review, documentation generation, OpenCode plugin compatibility, and cross-platform installation. Supports .NET, Python, TypeScript, Flutter, Go, Java, Node.js, React, Ruby, and Rust with plan-first execution and context-aware assistance.", | ||
@@ -8,3 +8,4 @@ "files": [ | ||
| ".opencode", | ||
| "opencode.json" | ||
| "opencode.json", | ||
| "scripts/lib" | ||
| ], | ||
@@ -36,2 +37,3 @@ "repository": { | ||
| "validate:context": "node scripts/check-context-size.js", | ||
| "validate:npx": "node scripts/test-npx-integrity.js", | ||
| "validate:all": "npm run doctor" | ||
@@ -38,0 +40,0 @@ }, |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
331035
5.14%97
3.19%1834
36.56%6
100%