Address review findings on the #2923 fail-closed parse fix: - Comment-only files produce ValueExpected with no JSON value; treat that as empty config instead of INVALID_JSONC. Partial object trees still throw. - readConfigLayers no longer lets one unparseable layer abort valid sibling layers. Mutations still fail closed on the custom/user write target. Co-authored-by: serkraser <serkraser@gmail.com>
667 lines
19 KiB
JavaScript
667 lines
19 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
import yaml from 'yaml';
|
|
import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
|
|
|
|
// ============== PATH CONSTANTS ==============
|
|
|
|
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
|
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
|
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'config.json');
|
|
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
|
|
|
// ============== SCOPE TYPE CONSTANTS ==============
|
|
|
|
const AGENT_SCOPE = {
|
|
USER: 'user',
|
|
PROJECT: 'project'
|
|
};
|
|
|
|
const COMMAND_SCOPE = {
|
|
USER: 'user',
|
|
PROJECT: 'project'
|
|
};
|
|
|
|
const SKILL_SCOPE = {
|
|
USER: 'user',
|
|
PROJECT: 'project'
|
|
};
|
|
|
|
// ============== DIRECTORY OPERATIONS ==============
|
|
|
|
function ensureDirs() {
|
|
if (!fs.existsSync(OPENCODE_CONFIG_DIR)) {
|
|
fs.mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true });
|
|
}
|
|
if (!fs.existsSync(AGENT_DIR)) {
|
|
fs.mkdirSync(AGENT_DIR, { recursive: true });
|
|
}
|
|
if (!fs.existsSync(COMMAND_DIR)) {
|
|
fs.mkdirSync(COMMAND_DIR, { recursive: true });
|
|
}
|
|
if (!fs.existsSync(SKILL_DIR)) {
|
|
fs.mkdirSync(SKILL_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// ============== MARKDOWN FILE OPERATIONS ==============
|
|
|
|
// Mirror of OpenCode's markdown frontmatter sanitizer (packages/opencode/src/
|
|
// config/markdown.ts): other coding agents accept unquoted colons in YAML
|
|
// values (e.g. `description: Build agent: creates builds`), which strict YAML
|
|
// rejects. Rewrite those values as block scalars and retry the parse, so files
|
|
// OpenCode accepts are parsed identically here.
|
|
function sanitizeFrontmatter(frontmatter) {
|
|
return frontmatter
|
|
.split(/\r?\n/)
|
|
.flatMap((line) => {
|
|
if (line.trim().startsWith('#') || line.trim() === '' || /^\s+/.test(line)) return [line];
|
|
const entry = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
|
|
if (!entry) return [line];
|
|
const value = entry[2].trim();
|
|
if (value === '' || value === '>' || value === '|' || value.startsWith('"') || value.startsWith("'")) return [line];
|
|
if (!value.includes(':')) return [line];
|
|
return [`${entry[1]}: |-`, ` ${value}`];
|
|
})
|
|
.join('\n');
|
|
}
|
|
|
|
function parseMdFile(filePath) {
|
|
const rawContent = fs.readFileSync(filePath, 'utf8');
|
|
// Strip a UTF-8 BOM so frontmatter is recognized regardless of the editor
|
|
// that saved the file.
|
|
const content = rawContent.charCodeAt(0) === 0xfeff ? rawContent.slice(1) : rawContent;
|
|
// The closing `---` may sit at end-of-file without a trailing newline.
|
|
// gray-matter (used by OpenCode) accepts that, so we must too: otherwise the
|
|
// whole file is treated as the prompt body and a later save rewrites the
|
|
// existing YAML block into the body, duplicating the frontmatter.
|
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/);
|
|
|
|
if (!match) {
|
|
return { frontmatter: {}, body: content.trim() };
|
|
}
|
|
|
|
let frontmatter = {};
|
|
try {
|
|
frontmatter = yaml.parse(match[1]) || {};
|
|
} catch (error) {
|
|
// Lenient fallback for frontmatter that strict YAML rejects but OpenCode
|
|
// still accepts (unquoted colons in scalar values).
|
|
try {
|
|
frontmatter = yaml.parse(sanitizeFrontmatter(match[1])) || {};
|
|
} catch {
|
|
console.warn(`Failed to parse markdown frontmatter ${filePath}, treating as empty:`, error);
|
|
frontmatter = {};
|
|
}
|
|
}
|
|
|
|
const body = match[2].trim();
|
|
return { frontmatter, body };
|
|
}
|
|
|
|
function writeMdFile(filePath, frontmatter, body) {
|
|
try {
|
|
const cleanedFrontmatter = Object.fromEntries(
|
|
Object.entries(frontmatter).filter(([, value]) => value != null)
|
|
);
|
|
const yamlStr = yaml.stringify(cleanedFrontmatter);
|
|
const content = `---\n${yamlStr}---\n\n${body}`;
|
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
console.log(`Successfully wrote markdown file: ${filePath}`);
|
|
} catch (error) {
|
|
console.error(`Failed to write markdown file ${filePath}:`, error);
|
|
throw new Error('Failed to write agent markdown file');
|
|
}
|
|
}
|
|
|
|
// ============== CONFIG FILE OPERATIONS ==============
|
|
|
|
function getProjectConfigCandidates(workingDirectory) {
|
|
if (!workingDirectory) return [];
|
|
return [
|
|
path.join(workingDirectory, 'opencode.json'),
|
|
path.join(workingDirectory, 'opencode.jsonc'),
|
|
path.join(workingDirectory, '.opencode', 'opencode.json'),
|
|
path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
|
|
];
|
|
}
|
|
|
|
function getProjectConfigPath(workingDirectory) {
|
|
if (!workingDirectory) return null;
|
|
|
|
const candidates = getProjectConfigCandidates(workingDirectory);
|
|
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return candidates[0];
|
|
}
|
|
|
|
function getConfigPaths(workingDirectory) {
|
|
return {
|
|
userPaths: [
|
|
path.join(OPENCODE_CONFIG_DIR, 'config.json'),
|
|
path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
|
|
path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
|
|
],
|
|
projectPath: getProjectConfigPath(workingDirectory),
|
|
// Resolve at call time so OPENCODE_CONFIG changes (and tests) take effect.
|
|
customPath: process.env.OPENCODE_CONFIG
|
|
? path.resolve(process.env.OPENCODE_CONFIG)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function getPrimaryUserConfigPath(userPaths) {
|
|
for (const userPath of userPaths) {
|
|
if (fs.existsSync(userPath)) {
|
|
return userPath;
|
|
}
|
|
}
|
|
|
|
return CONFIG_FILE;
|
|
}
|
|
|
|
const INVALID_JSONC = 'INVALID_JSONC';
|
|
|
|
function isInvalidJsoncError(error) {
|
|
return Boolean(error && typeof error === 'object' && error.code === INVALID_JSONC);
|
|
}
|
|
|
|
function formatJsoncParseError(filePath, errors) {
|
|
const first = Array.isArray(errors) && errors.length > 0 ? errors[0] : null;
|
|
const location = first && Number.isFinite(first.offset)
|
|
? ` (${printParseErrorCode(first.error)} at offset ${first.offset})`
|
|
: '';
|
|
return `OpenCode configuration at ${filePath} contains invalid JSONC and cannot be loaded safely${location}`;
|
|
}
|
|
|
|
function parseConfigObject(content, filePath) {
|
|
const errors = [];
|
|
const parsed = parseJsonc(content, errors, { allowTrailingComma: true });
|
|
// Comment-only / no JSON value: jsonc-parser returns undefined plus ValueExpected.
|
|
// That is empty config, not a partial tree. The data-loss bug is errors + object.
|
|
if (parsed === undefined) {
|
|
return {};
|
|
}
|
|
if (errors.length > 0 || !parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
const error = new Error(formatJsoncParseError(filePath, errors));
|
|
error.code = INVALID_JSONC;
|
|
throw error;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function readConfigFile(filePath) {
|
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
return {};
|
|
}
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const normalized = content.trim();
|
|
if (!normalized) {
|
|
return {};
|
|
}
|
|
// Refuse partial jsonc-parser trees. Ignoring errors previously let mutations
|
|
// rewrite a truncated object (often only `$schema`) over the full config.
|
|
return parseConfigObject(normalized, filePath);
|
|
} catch (error) {
|
|
if (isInvalidJsoncError(error)) {
|
|
throw error;
|
|
}
|
|
console.error(`Failed to read config file: ${filePath}`, error);
|
|
throw new Error('Failed to read OpenCode configuration');
|
|
}
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function mergeConfigs(base, override) {
|
|
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
return override;
|
|
}
|
|
const result = { ...base };
|
|
for (const [key, value] of Object.entries(override)) {
|
|
if (key in result) {
|
|
const baseValue = result[key];
|
|
if (isPlainObject(baseValue) && isPlainObject(value)) {
|
|
result[key] = mergeConfigs(baseValue, value);
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function readConfigLayer(filePath) {
|
|
try {
|
|
return { config: readConfigFile(filePath), error: null };
|
|
} catch (error) {
|
|
if (isInvalidJsoncError(error)) {
|
|
console.error(error.message);
|
|
return { config: {}, error };
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function readConfigLayers(workingDirectory) {
|
|
const { userPaths, projectPath, customPath } = getConfigPaths(workingDirectory);
|
|
const userPath = getPrimaryUserConfigPath(userPaths);
|
|
const userLayer = readConfigLayer(userPath);
|
|
const projectLayer = readConfigLayer(projectPath);
|
|
const customLayer = readConfigLayer(customPath);
|
|
const mergedConfig = mergeConfigs(
|
|
mergeConfigs(userLayer.config, projectLayer.config),
|
|
customLayer.config,
|
|
);
|
|
|
|
const layerErrors = [];
|
|
if (userLayer.error) {
|
|
layerErrors.push({ path: userPath, code: userLayer.error.code, message: userLayer.error.message });
|
|
}
|
|
if (projectLayer.error && projectPath) {
|
|
layerErrors.push({ path: projectPath, code: projectLayer.error.code, message: projectLayer.error.message });
|
|
}
|
|
if (customLayer.error && customPath) {
|
|
layerErrors.push({ path: customPath, code: customLayer.error.code, message: customLayer.error.message });
|
|
}
|
|
|
|
return {
|
|
userConfig: userLayer.config,
|
|
projectConfig: projectLayer.config,
|
|
customConfig: customLayer.config,
|
|
mergedConfig,
|
|
paths: { userPath, projectPath, customPath },
|
|
layerErrors,
|
|
};
|
|
}
|
|
|
|
function readConfig(workingDirectory) {
|
|
return readConfigLayers(workingDirectory).mergedConfig;
|
|
}
|
|
|
|
function getConfigForPath(layers, targetPath) {
|
|
if (!targetPath) {
|
|
return layers.userConfig;
|
|
}
|
|
if (layers.paths.customPath && targetPath === layers.paths.customPath) {
|
|
return layers.customConfig;
|
|
}
|
|
if (layers.paths.projectPath && targetPath === layers.paths.projectPath) {
|
|
return layers.projectConfig;
|
|
}
|
|
return layers.userConfig;
|
|
}
|
|
|
|
function writeConfig(config, filePath = CONFIG_FILE) {
|
|
try {
|
|
if (fs.existsSync(filePath)) {
|
|
// Defense in depth: never overwrite a file we cannot fully parse.
|
|
const existing = fs.readFileSync(filePath, 'utf8').trim();
|
|
if (existing) {
|
|
parseConfigObject(existing, filePath);
|
|
}
|
|
|
|
const backupFile = `${filePath}.openchamber.backup`;
|
|
fs.copyFileSync(filePath, backupFile);
|
|
console.log(`Created config backup: ${backupFile}`);
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
console.log(`Successfully wrote config file: ${filePath}`);
|
|
} catch (error) {
|
|
if (isInvalidJsoncError(error)) {
|
|
throw error;
|
|
}
|
|
console.error(`Failed to write config file: ${filePath}`, error);
|
|
throw new Error('Failed to write OpenCode configuration');
|
|
}
|
|
}
|
|
|
|
function getLayerError(layers, filePath) {
|
|
if (!filePath || !Array.isArray(layers?.layerErrors)) {
|
|
return null;
|
|
}
|
|
return layers.layerErrors.find((entry) => entry.path === filePath) || null;
|
|
}
|
|
|
|
function throwIfLayerError(layers, filePath) {
|
|
const failed = getLayerError(layers, filePath);
|
|
if (!failed) {
|
|
return;
|
|
}
|
|
const error = new Error(failed.message);
|
|
error.code = failed.code;
|
|
throw error;
|
|
}
|
|
|
|
function getJsonEntrySource(layers, sectionKey, entryName) {
|
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
if (paths.customPath) {
|
|
throwIfLayerError(layers, paths.customPath);
|
|
const customSection = customConfig?.[sectionKey]?.[entryName];
|
|
if (customSection !== undefined) {
|
|
return { section: customSection, config: customConfig, path: paths.customPath, exists: true };
|
|
}
|
|
}
|
|
|
|
if (paths.projectPath && !getLayerError(layers, paths.projectPath)) {
|
|
const projectSection = projectConfig?.[sectionKey]?.[entryName];
|
|
if (projectSection !== undefined) {
|
|
return { section: projectSection, config: projectConfig, path: paths.projectPath, exists: true };
|
|
}
|
|
}
|
|
|
|
throwIfLayerError(layers, paths.userPath);
|
|
const userSection = userConfig?.[sectionKey]?.[entryName];
|
|
if (userSection !== undefined) {
|
|
return { section: userSection, config: userConfig, path: paths.userPath, exists: true };
|
|
}
|
|
|
|
return { section: null, config: null, path: null, exists: false };
|
|
}
|
|
|
|
function getJsonWriteTarget(layers, preferredScope) {
|
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
if (paths.customPath) {
|
|
throwIfLayerError(layers, paths.customPath);
|
|
return { config: customConfig, path: paths.customPath };
|
|
}
|
|
if (preferredScope === AGENT_SCOPE.PROJECT && paths.projectPath) {
|
|
throwIfLayerError(layers, paths.projectPath);
|
|
return { config: projectConfig, path: paths.projectPath };
|
|
}
|
|
throwIfLayerError(layers, paths.userPath);
|
|
return { config: userConfig, path: paths.userPath };
|
|
}
|
|
|
|
// ============== GIT/WORKTREE HELPERS ==============
|
|
|
|
function getAncestors(startDir, stopDir) {
|
|
if (!startDir) return [];
|
|
const result = [];
|
|
let current = path.resolve(startDir);
|
|
const resolvedStop = stopDir ? path.resolve(stopDir) : null;
|
|
|
|
while (true) {
|
|
result.push(current);
|
|
if (resolvedStop && current === resolvedStop) {
|
|
break;
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) {
|
|
break;
|
|
}
|
|
current = parent;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function findWorktreeRoot(startDir) {
|
|
if (!startDir) return null;
|
|
let current = path.resolve(startDir);
|
|
|
|
while (true) {
|
|
if (fs.existsSync(path.join(current, '.git'))) {
|
|
return current;
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) {
|
|
return null;
|
|
}
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
// ============== PROMPT FILE HELPERS ==============
|
|
|
|
function isPromptFileReference(value) {
|
|
if (typeof value !== 'string') {
|
|
return false;
|
|
}
|
|
return PROMPT_FILE_PATTERN.test(value.trim());
|
|
}
|
|
|
|
function resolvePromptFilePath(reference) {
|
|
const match = typeof reference === 'string' ? reference.trim().match(PROMPT_FILE_PATTERN) : null;
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
let target = match[1].trim();
|
|
if (!target) {
|
|
return null;
|
|
}
|
|
|
|
if (target.startsWith('./')) {
|
|
target = target.slice(2);
|
|
target = path.join(OPENCODE_CONFIG_DIR, target);
|
|
} else if (!path.isAbsolute(target)) {
|
|
target = path.join(OPENCODE_CONFIG_DIR, target);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
function writePromptFile(filePath, content) {
|
|
const dir = path.dirname(filePath);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(filePath, content ?? '', 'utf8');
|
|
console.log(`Updated prompt file: ${filePath}`);
|
|
}
|
|
|
|
// ============== SKILL FILE OPERATIONS ==============
|
|
|
|
function walkSkillMdFiles(rootDir) {
|
|
if (!rootDir || !fs.existsSync(rootDir)) return [];
|
|
|
|
const results = [];
|
|
const walk = (dir) => {
|
|
let entries = [];
|
|
try {
|
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walk(fullPath);
|
|
continue;
|
|
}
|
|
if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
};
|
|
|
|
walk(rootDir);
|
|
return results;
|
|
}
|
|
|
|
function addSkillFromMdFile(skillsMap, skillMdPath, scope, source) {
|
|
let parsed;
|
|
try {
|
|
parsed = parseMdFile(skillMdPath);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const name = typeof parsed.frontmatter?.name === 'string'
|
|
? parsed.frontmatter.name.trim()
|
|
: '';
|
|
const description = typeof parsed.frontmatter?.description === 'string'
|
|
? parsed.frontmatter.description
|
|
: '';
|
|
|
|
if (!name) {
|
|
return;
|
|
}
|
|
|
|
skillsMap.set(name, {
|
|
name,
|
|
path: skillMdPath,
|
|
scope,
|
|
source,
|
|
description,
|
|
});
|
|
}
|
|
|
|
function resolveSkillSearchDirectories(workingDirectory) {
|
|
const directories = [];
|
|
const pushDir = (dir) => {
|
|
if (!dir) return;
|
|
const resolved = path.resolve(dir);
|
|
if (!directories.includes(resolved)) {
|
|
directories.push(resolved);
|
|
}
|
|
};
|
|
|
|
pushDir(OPENCODE_CONFIG_DIR);
|
|
|
|
if (workingDirectory) {
|
|
const worktreeRoot = findWorktreeRoot(workingDirectory) || path.resolve(workingDirectory);
|
|
const projectDirs = getAncestors(workingDirectory, worktreeRoot)
|
|
.map((dir) => path.join(dir, '.opencode'));
|
|
projectDirs.forEach(pushDir);
|
|
}
|
|
|
|
pushDir(path.join(os.homedir(), '.opencode'));
|
|
|
|
const customConfigDir = process.env.OPENCODE_CONFIG_DIR
|
|
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
|
|
: null;
|
|
pushDir(customConfigDir);
|
|
|
|
return directories;
|
|
}
|
|
|
|
function listSkillSupportingFiles(skillDir) {
|
|
if (!fs.existsSync(skillDir)) {
|
|
return [];
|
|
}
|
|
|
|
const files = [];
|
|
|
|
function walkDir(dir, relativePath = '') {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
|
|
if (entry.isDirectory()) {
|
|
walkDir(fullPath, relPath);
|
|
} else if (entry.name !== 'SKILL.md') {
|
|
files.push({
|
|
name: entry.name,
|
|
path: relPath,
|
|
fullPath: fullPath
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
walkDir(skillDir);
|
|
return files;
|
|
}
|
|
|
|
function assertPathWithinSkillDir(skillDir, relativePath) {
|
|
const root = fs.realpathSync(skillDir);
|
|
const target = path.resolve(root, relativePath);
|
|
const relative = path.relative(root, target);
|
|
const isWithin = relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
|
|
if (!isWithin) {
|
|
const error = new Error('Access to file denied');
|
|
error.code = 'EACCES';
|
|
throw error;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
function readSkillSupportingFile(skillDir, relativePath) {
|
|
const fullPath = assertPathWithinSkillDir(skillDir, relativePath);
|
|
if (!fs.existsSync(fullPath)) {
|
|
return null;
|
|
}
|
|
return fs.readFileSync(fullPath, 'utf8');
|
|
}
|
|
|
|
function writeSkillSupportingFile(skillDir, relativePath, content) {
|
|
const fullPath = assertPathWithinSkillDir(skillDir, relativePath);
|
|
const dir = path.dirname(fullPath);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(fullPath, content, 'utf8');
|
|
}
|
|
|
|
function deleteSkillSupportingFile(skillDir, relativePath) {
|
|
const root = fs.realpathSync(skillDir);
|
|
const fullPath = assertPathWithinSkillDir(skillDir, relativePath);
|
|
if (fs.existsSync(fullPath)) {
|
|
fs.unlinkSync(fullPath);
|
|
let parentDir = path.dirname(fullPath);
|
|
while (parentDir !== root) {
|
|
try {
|
|
const entries = fs.readdirSync(parentDir);
|
|
if (entries.length === 0) {
|
|
fs.rmdirSync(parentDir);
|
|
parentDir = path.dirname(parentDir);
|
|
} else {
|
|
break;
|
|
}
|
|
} catch {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export {
|
|
OPENCODE_CONFIG_DIR,
|
|
AGENT_DIR,
|
|
COMMAND_DIR,
|
|
SKILL_DIR,
|
|
CONFIG_FILE,
|
|
AGENT_SCOPE,
|
|
COMMAND_SCOPE,
|
|
SKILL_SCOPE,
|
|
ensureDirs,
|
|
parseMdFile,
|
|
writeMdFile,
|
|
readConfigFile,
|
|
isPlainObject,
|
|
readConfigLayers,
|
|
readConfig,
|
|
getConfigForPath,
|
|
writeConfig,
|
|
getJsonEntrySource,
|
|
getJsonWriteTarget,
|
|
getAncestors,
|
|
findWorktreeRoot,
|
|
isPromptFileReference,
|
|
resolvePromptFilePath,
|
|
writePromptFile,
|
|
walkSkillMdFiles,
|
|
addSkillFromMdFile,
|
|
resolveSkillSearchDirectories,
|
|
listSkillSupportingFiles,
|
|
readSkillSupportingFile,
|
|
writeSkillSupportingFile,
|
|
deleteSkillSupportingFile,
|
|
};
|