Skip to content

Commit 68ca2e4

Browse files
EthanMiller0xclaude
andcommitted
fix: community plugin loading, settings path, and onboard init
- Load community plugins from registry on startup (main.ts) - Fix path resolution for local and npm plugins (absolute paths, ESM entry point) - Scan node_modules when plugin name differs from npm package name - Align settings basePath to ~/.openacp/plugins/data/ across all CLI commands - Initialize plugin system in onboard command - Add debug logging for settings resolution in lifecycle-manager Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c85d5b commit 68ca2e4

4 files changed

Lines changed: 62 additions & 12 deletions

File tree

src/cli/commands/onboard.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,18 @@ const PLUGINS_DATA_DIR = path.join(OPENACP_DIR, 'plugins', 'data')
66
const REGISTRY_PATH = path.join(OPENACP_DIR, 'plugins.json')
77

88
export async function cmdOnboard(): Promise<void> {
9+
const os = await import('node:os')
10+
const path = await import('node:path')
911
const { ConfigManager } = await import('../../core/config/config.js')
12+
const { SettingsManager } = await import('../../core/plugin/settings-manager.js')
13+
const { PluginRegistry } = await import('../../core/plugin/plugin-registry.js')
14+
1015
const cm = new ConfigManager()
16+
const pluginsDataDir = path.join(os.homedir(), '.openacp', 'plugins', 'data')
17+
const registryPath = path.join(os.homedir(), '.openacp', 'plugins.json')
18+
const settingsManager = new SettingsManager(pluginsDataDir)
19+
const pluginRegistry = new PluginRegistry(registryPath)
20+
await pluginRegistry.load()
1121

1222
if (await cm.exists()) {
1323
const { runReconfigure } = await import('../../core/setup/index.js')

src/cli/commands/plugins.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async function configurePlugin(name: string): Promise<void> {
184184
process.exit(1)
185185
}
186186

187-
const basePath = path.join(os.homedir(), '.openacp', 'plugins')
187+
const basePath = path.join(os.homedir(), '.openacp', 'plugins', 'data')
188188
const settingsManager = new SettingsManager(basePath)
189189
const ctx = createInstallContext({ pluginName: name, settingsManager, basePath })
190190

@@ -261,7 +261,7 @@ async function installPlugin(input: string): Promise<void> {
261261
const { corePlugins } = await import('../../plugins/core-plugins.js')
262262
const builtinPlugin = corePlugins.find(p => p.name === pkgName)
263263

264-
const basePath = path.join(os.homedir(), '.openacp', 'plugins')
264+
const basePath = path.join(os.homedir(), '.openacp', 'plugins', 'data')
265265
const settingsManager = new SettingsManager(basePath)
266266
const registryPath = path.join(os.homedir(), '.openacp', 'plugins.json')
267267
const pluginRegistry = new PluginRegistry(registryPath)
@@ -302,8 +302,10 @@ async function installPlugin(input: string): Promise<void> {
302302

303303
// Read installed plugin's package.json for compatibility check
304304
const cliVersion = getCurrentVersion()
305+
const isLocalPath = pkgName.startsWith('/') || pkgName.startsWith('.')
305306
try {
306-
const installedPkgPath = path.join(nodeModulesDir, pkgName, 'package.json')
307+
const pluginRoot = isLocalPath ? path.resolve(pkgName) : path.join(nodeModulesDir, pkgName)
308+
const installedPkgPath = path.join(pluginRoot, 'package.json')
307309
const { readFileSync } = await import('node:fs')
308310
const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf-8'))
309311

@@ -318,7 +320,7 @@ async function installPlugin(input: string): Promise<void> {
318320
}
319321

320322
// Try to load and run install hook
321-
const pluginModule = await import(path.join(nodeModulesDir, pkgName, installedPkg.main ?? 'dist/index.js'))
323+
const pluginModule = await import(path.join(pluginRoot, installedPkg.main ?? 'dist/index.js'))
322324
const plugin = pluginModule.default
323325

324326
if (plugin?.install) {
@@ -377,7 +379,7 @@ async function uninstallPlugin(name: string, purge: boolean): Promise<void> {
377379
if (plugin?.uninstall) {
378380
const { SettingsManager } = await import('../../core/plugin/settings-manager.js')
379381
const { createInstallContext } = await import('../../core/plugin/install-context.js')
380-
const basePath = path.join(os.homedir(), '.openacp', 'plugins')
382+
const basePath = path.join(os.homedir(), '.openacp', 'plugins', 'data')
381383
const settingsManager = new SettingsManager(basePath)
382384
const ctx = createInstallContext({ pluginName: name, settingsManager, basePath })
383385
await plugin.uninstall(ctx, { purge })

src/core/plugin/lifecycle-manager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,14 @@ export class LifecycleManager {
205205
let pluginConfig: Record<string, unknown>
206206
if (this.settingsManager) {
207207
pluginConfig = await this.settingsManager.loadSettings(plugin.name)
208+
const settingsPath = this.settingsManager.getSettingsPath(plugin.name)
209+
this.getPluginLogger(plugin.name).debug(`Settings loaded from ${settingsPath}: ${Object.keys(pluginConfig).length} keys`)
208210
if (Object.keys(pluginConfig).length === 0) {
209211
pluginConfig = resolvePluginConfig(plugin.name, this.config)
210212
}
211213
} else {
212214
pluginConfig = resolvePluginConfig(plugin.name, this.config)
215+
this.getPluginLogger(plugin.name).debug('No settingsManager, using legacy config')
213216
}
214217

215218
// Create context for this plugin

src/main.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,49 @@ export async function startServer(opts?: StartServerOptions) {
140140
try {
141141
let modulePath: string
142142

143-
if (entry.source === 'local') {
144-
// Local plugin: name is the filesystem path or resolves directly
145-
modulePath = name.startsWith('/') || name.startsWith('.')
146-
? path.resolve(name)
147-
: path.join(OPENACP_DIR, 'plugins', 'node_modules', name)
143+
if (name.startsWith('/') || name.startsWith('.')) {
144+
// Absolute or relative path (local install via `plugin add /path/to/plugin`)
145+
const resolved = path.resolve(name)
146+
const pkgPath = path.join(resolved, 'package.json')
147+
const pkg = JSON.parse(await fs.promises.readFile(pkgPath, 'utf-8'))
148+
modulePath = path.join(resolved, pkg.main || 'dist/index.js')
148149
} else {
149-
// npm plugin: installed under ~/.openacp/plugins/node_modules/<name>
150-
modulePath = path.join(OPENACP_DIR, 'plugins', 'node_modules', name)
150+
// npm package: try direct name first, then scan node_modules for matching plugin name
151+
const nodeModulesDir = path.join(OPENACP_DIR, 'plugins', 'node_modules')
152+
let pkgDir = path.join(nodeModulesDir, name)
153+
154+
if (!fs.existsSync(path.join(pkgDir, 'package.json'))) {
155+
// Plugin name doesn't match npm package name — scan installed packages
156+
let found = false
157+
const scopes = fs.readdirSync(nodeModulesDir).filter(d => d.startsWith('@'))
158+
for (const scope of scopes) {
159+
const scopeDir = path.join(nodeModulesDir, scope)
160+
const pkgs = fs.readdirSync(scopeDir)
161+
for (const pkg of pkgs) {
162+
const candidateDir = path.join(scopeDir, pkg)
163+
const candidatePkgPath = path.join(candidateDir, 'package.json')
164+
if (fs.existsSync(candidatePkgPath)) {
165+
try {
166+
const candidatePkg = JSON.parse(fs.readFileSync(candidatePkgPath, 'utf-8'))
167+
const mainPath = path.join(candidateDir, candidatePkg.main || 'dist/index.js')
168+
if (fs.existsSync(mainPath)) {
169+
const testMod = await import(mainPath)
170+
if (testMod.default?.name === name) {
171+
pkgDir = candidateDir
172+
found = true
173+
break
174+
}
175+
}
176+
} catch { /* skip */ }
177+
}
178+
}
179+
if (found) break
180+
}
181+
}
182+
183+
const pkgJsonPath = path.join(pkgDir, 'package.json')
184+
const pkg = JSON.parse(await fs.promises.readFile(pkgJsonPath, 'utf-8'))
185+
modulePath = path.join(pkgDir, pkg.main || 'dist/index.js')
151186
}
152187

153188
log.debug({ plugin: name, modulePath }, 'Loading community plugin')

0 commit comments

Comments
 (0)