Skip to content

Commit 4568699

Browse files
authored
Merge pull request #1 from techdivision/develop
feat: v0.1.0 - User config directory support and singlerepo plugin fix
2 parents 5890bb6 + d3dd4e2 commit 4568699

5 files changed

Lines changed: 103 additions & 44 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ console.log(`${colors.green} + ${message}${colors.reset}`);
160160
### Plugin Discovery (lib/discovery.js)
161161
162162
Plugins are discovered from two locations:
163-
1. **Global**: `npm config get prefix`/lib/node_modules/
163+
1. **User config**: `~/.config/opencode/node_modules/`
164164
2. **Local**: `{targetDir}/node_modules/`
165165
166-
Priority: **last wins** (local overrides global)
166+
Priority: **last wins** (local overrides user)
167167
168168
### Plugin Types
169169

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2025-03-04
9+
10+
### Added
11+
- User config directory support (`~/.config/opencode/node_modules/`)
12+
- Direct linking to `~/.config/opencode` when running from user config directory
13+
- Skip `opencode.json` creation in user config directory (uses `config.json`)
14+
15+
### Fixed
16+
- Singlerepo plugin source directory detection (use `descriptor.type` instead of `isUnified`)
17+
18+
### Changed
19+
- Removed npm global prefix lookup - plugins are now discovered from user config and local only

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: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Plugin Discovery Module
33
*
44
* Discovers OpenCode plugins from two locations (priority: last wins):
5-
* 1. Global: npm global prefix (npm install -g)
5+
* 1. User config: ~/.config/opencode/node_modules/
66
* 2. Local: {targetDir}/node_modules/
77
*
88
* Each location can contain two types of packages:
@@ -13,52 +13,54 @@
1313
import fs from 'fs';
1414
import os from 'os';
1515
import path from 'path';
16-
import { execSync } from 'child_process';
1716

1817
/**
19-
* Gets the npm global prefix directory.
20-
* This is where `npm install -g` installs packages.
18+
* Gets the user config directory for OpenCode plugins.
19+
* This is where user-level plugins are installed.
2120
*
22-
* @returns {string|null} Path to npm global prefix or null if not determinable
21+
* @returns {string} Path to ~/.config/opencode/node_modules
2322
*/
24-
function getGlobalDir() {
25-
try {
26-
const prefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim();
27-
return path.join(prefix, 'lib', 'node_modules');
28-
} catch {
29-
return null;
30-
}
23+
function getUserConfigDir() {
24+
return path.join(os.homedir(), '.config', 'opencode', 'node_modules');
3125
}
3226

3327
/**
3428
* Discovers all available plugins from all locations.
3529
*
3630
* Priority (last wins):
37-
* 1. Global: npm global prefix (npm install -g)
31+
* 1. User config: ~/.config/opencode/node_modules/
3832
* 2. Local: {targetDir}/node_modules/
3933
*
34+
* If targetDir is the user config directory (~/.config/opencode), only local
35+
* is scanned to avoid duplicate scanning of the same directory.
36+
*
4037
* @param {string} targetDir - The .opencode directory (e.g., /project/.opencode)
4138
* @returns {Map<string, PluginDescriptor>} Map of plugin name to descriptor
4239
*/
4340
export function discoverPlugins(targetDir) {
44-
const globalDir = getGlobalDir();
41+
const userConfigDir = getUserConfigDir();
42+
const localNodeModules = path.join(targetDir, 'node_modules');
4543

4644
// Scan all locations (order matters: last wins)
4745
const allPlugins = new Map();
4846

49-
// 1. Global (lowest priority)
50-
if (globalDir) {
51-
const globalPlugins = scanLocation(globalDir, 'global');
52-
for (const [name, descriptor] of globalPlugins.entries()) {
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()) {
5354
allPlugins.set(name, descriptor);
5455
}
5556
}
5657

57-
// 2. Local (highest priority)
58-
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);
5961
for (const [name, descriptor] of localPlugins.entries()) {
6062
if (allPlugins.has(name)) {
61-
console.log(` ⚠️ Plugin "${name}" found in local, overrides global`);
63+
console.log(` ⚠️ Plugin "${name}" found in local, overrides user`);
6264
}
6365
allPlugins.set(name, descriptor);
6466
}

lib/linker.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,23 +68,23 @@ export function normalizeContentType(type) {
6868
/**
6969
* Gets source directory for a plugin based on type.
7070
*
71-
* For unified plugins: content is in package root (agent/, command/, etc.)
72-
* For legacy plugins: content is in .opencode/ subdirectory
71+
* For singlerepo plugins: content is in package root (agents/, commands/, etc.)
72+
* For monorepo plugins: content is in .opencode/ subdirectory
7373
*
7474
* @param {PluginDescriptor} descriptor - Plugin descriptor
75-
* @param {string} type - Content type (agent, command, skill, etc.)
75+
* @param {string} type - Content type (agents, commands, skills, etc.)
7676
* @returns {string|null} Source directory path or null if not exists
7777
*/
7878
export function getSourceDir(descriptor, type) {
7979
if (!descriptor.contentTypes.includes(type)) {
8080
return null;
8181
}
8282

83-
if (descriptor.isUnified) {
84-
// Unified plugin: content in package root
83+
if (descriptor.type === 'singlerepo') {
84+
// Singlerepo plugin: content in package root
8585
return path.join(descriptor.rootDir, type);
8686
} else {
87-
// Legacy plugin: content in .opencode/ subdirectory
87+
// Monorepo plugin: content in .opencode/ subdirectory
8888
return path.join(descriptor.rootDir, '.opencode', type);
8989
}
9090
}

0 commit comments

Comments
 (0)