Skip to content

Commit 27a5c1a

Browse files
committed
feat: support linking directly to ~/.config/opencode
When running from the OpenCode user config directory (~/.config/opencode), plugins are now linked directly there instead of to a .opencode subdirectory. Changes: - Add isUserConfigDir() helper to detect user config directory - Update resolveTargetDir() to return 'user-config' source - Skip opencode.json creation in user-config dir (uses config.json) - Skip duplicate scan in discoverPlugins() when targetDir is user-config
1 parent 12fbf54 commit 27a5c1a

2 files changed

Lines changed: 69 additions & 21 deletions

File tree

bin/opencode-link.js

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,29 @@ function formatPluginLabel(descriptor) {
102102

103103
// Target directory for linking (set in main() after argument parsing)
104104
let TARGET_DIR = null;
105+
let TARGET_SOURCE = null;
105106
let PLUGINS = null;
106107
let CONTENT_TYPES = null;
107108

109+
/**
110+
* Gets the OpenCode user config directory path.
111+
*
112+
* @returns {string} Path to ~/.config/opencode
113+
*/
114+
function getUserConfigDir() {
115+
return path.join(os.homedir(), '.config', 'opencode');
116+
}
117+
118+
/**
119+
* Checks if the given directory is the OpenCode user config directory.
120+
*
121+
* @param {string} dir - Directory to check
122+
* @returns {boolean} True if dir is ~/.config/opencode
123+
*/
124+
function isUserConfigDir(dir) {
125+
return path.resolve(dir) === getUserConfigDir();
126+
}
127+
108128
/**
109129
* Searches for opencode.json starting from current directory and going up.
110130
*/
@@ -124,15 +144,30 @@ function findOpencodeConfig() {
124144

125145
/**
126146
* Resolves the target directory for linking.
147+
*
148+
* Detection order:
149+
* 1. CLI --target-dir flag
150+
* 2. OpenCode user config directory (~/.config/opencode)
151+
* 3. Directory with opencode.json (search upwards)
152+
* 4. .opencode subdirectory
153+
* 5. Default: {cwd}/.opencode
127154
*/
128155
function resolveTargetDir(cliTargetDir) {
129156
if (cliTargetDir) {
157+
const resolved = path.resolve(process.cwd(), cliTargetDir);
130158
return {
131-
targetDir: path.resolve(process.cwd(), cliTargetDir),
132-
source: 'cli',
159+
targetDir: resolved,
160+
source: isUserConfigDir(resolved) ? 'user-config' : 'cli',
133161
};
134162
}
135163

164+
const cwd = process.cwd();
165+
166+
// Check if we're in the OpenCode user config directory
167+
if (isUserConfigDir(cwd)) {
168+
return { targetDir: cwd, source: 'user-config' };
169+
}
170+
136171
const configDir = findOpencodeConfig();
137172
if (configDir) {
138173
return {
@@ -141,8 +176,6 @@ function resolveTargetDir(cliTargetDir) {
141176
};
142177
}
143178

144-
const cwd = process.cwd();
145-
146179
// Check if we're already in a .opencode directory
147180
if (path.basename(cwd) === '.opencode') {
148181
return { targetDir: cwd, source: 'auto' };
@@ -199,15 +232,17 @@ function ensureDirectoryStructure() {
199232
}
200233
}
201234

202-
// Ensure opencode.json exists
203-
const configPath = path.join(TARGET_DIR, 'opencode.json');
204-
if (!fs.existsSync(configPath)) {
205-
const defaultConfig = {
206-
$schema: 'https://opencode.ai/config.json',
207-
instructions: ['guideline/*.md'],
208-
};
209-
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
210-
logSuccess('opencode.json created');
235+
// Ensure opencode.json exists (skip for user-config directory which uses config.json)
236+
if (TARGET_SOURCE !== 'user-config') {
237+
const configPath = path.join(TARGET_DIR, 'opencode.json');
238+
if (!fs.existsSync(configPath)) {
239+
const defaultConfig = {
240+
$schema: 'https://opencode.ai/config.json',
241+
instructions: ['guideline/*.md'],
242+
};
243+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
244+
logSuccess('opencode.json created');
245+
}
211246
}
212247
}
213248

@@ -629,16 +664,19 @@ async function main() {
629664
// Resolve target directory
630665
const { targetDir, source } = resolveTargetDir(cliTargetDir);
631666
TARGET_DIR = targetDir;
667+
TARGET_SOURCE = source;
632668

633-
// Discover plugins (global + local)
669+
// Discover plugins
634670
PLUGINS = discoverPlugins(TARGET_DIR);
635671
CONTENT_TYPES = getContentTypes(PLUGINS);
636672

637673
log('\nOpenCode Plugin Linker', 'bright');
638674
log('='.repeat(50), 'gray');
639675

640676
// Show target directory info
641-
if (source === 'cli') {
677+
if (source === 'user-config') {
678+
log(`Target: ${TARGET_DIR} (user config directory)`, 'cyan');
679+
} else if (source === 'cli') {
642680
log(`Target: ${TARGET_DIR} (from --target-dir)`, 'cyan');
643681
} else if (source === 'auto') {
644682
log(`Target: ${TARGET_DIR} (auto-detected opencode.json)`, 'cyan');

lib/discovery.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,33 @@ function getUserConfigDir() {
3131
* 1. User config: ~/.config/opencode/node_modules/
3232
* 2. Local: {targetDir}/node_modules/
3333
*
34+
* If targetDir is the user config directory (~/.config/opencode), only local
35+
* is scanned to avoid duplicate scanning of the same directory.
36+
*
3437
* @param {string} targetDir - The .opencode directory (e.g., /project/.opencode)
3538
* @returns {Map<string, PluginDescriptor>} Map of plugin name to descriptor
3639
*/
3740
export function discoverPlugins(targetDir) {
3841
const userConfigDir = getUserConfigDir();
42+
const localNodeModules = path.join(targetDir, 'node_modules');
3943

4044
// Scan all locations (order matters: last wins)
4145
const allPlugins = new Map();
4246

43-
// 1. User config (lower priority)
44-
const userPlugins = scanLocation(userConfigDir, 'user');
45-
for (const [name, descriptor] of userPlugins.entries()) {
46-
allPlugins.set(name, descriptor);
47+
// Check if local === user-config (when running from ~/.config/opencode)
48+
const isUserConfigTarget = path.resolve(localNodeModules) === path.resolve(userConfigDir);
49+
50+
// 1. User config (lower priority) - skip if same as local
51+
if (!isUserConfigTarget) {
52+
const userPlugins = scanLocation(userConfigDir, 'user');
53+
for (const [name, descriptor] of userPlugins.entries()) {
54+
allPlugins.set(name, descriptor);
55+
}
4756
}
4857

49-
// 2. Local (highest priority)
50-
const localPlugins = scanLocation(path.join(targetDir, 'node_modules'), 'local');
58+
// 2. Local (highest priority) - labeled as 'user' if it's the user-config dir
59+
const localLabel = isUserConfigTarget ? 'user' : 'local';
60+
const localPlugins = scanLocation(localNodeModules, localLabel);
5161
for (const [name, descriptor] of localPlugins.entries()) {
5262
if (allPlugins.has(name)) {
5363
console.log(` ⚠️ Plugin "${name}" found in local, overrides user`);

0 commit comments

Comments
 (0)