diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 35d769e4..86bf8239 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Configured extension directories now respect `package.json` manifests: when a directory is specified in `settings.json` extensions array, it now checks for `pi.extensions` in `package.json` first, then falls back to `index.ts`/`index.js` + ## [0.37.1] - 2026-01-05 ### Fixed diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index b043eb31..acd446f2 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -478,6 +478,47 @@ function isExtensionFile(name: string): boolean { return name.endsWith(".ts") || name.endsWith(".js"); } +/** + * Resolve extension entry points from a directory. + * + * Checks for: + * 1. package.json with "pi.extensions" field -> returns declared paths + * 2. index.ts or index.js -> returns the index file + * + * Returns resolved paths or null if no entry points found. + */ +function resolveExtensionEntries(dir: string): string[] | null { + // Check for package.json with "pi" field first + const packageJsonPath = path.join(dir, "package.json"); + if (fs.existsSync(packageJsonPath)) { + const manifest = readPiManifest(packageJsonPath); + if (manifest?.extensions?.length) { + const entries: string[] = []; + for (const extPath of manifest.extensions) { + const resolvedExtPath = path.resolve(dir, extPath); + if (fs.existsSync(resolvedExtPath)) { + entries.push(resolvedExtPath); + } + } + if (entries.length > 0) { + return entries; + } + } + } + + // Check for index.ts or index.js + const indexTs = path.join(dir, "index.ts"); + const indexJs = path.join(dir, "index.js"); + if (fs.existsSync(indexTs)) { + return [indexTs]; + } + if (fs.existsSync(indexJs)) { + return [indexJs]; + } + + return null; +} + /** * Discover extensions in a directory. * @@ -509,29 +550,9 @@ function discoverExtensionsInDir(dir: string): string[] { // 2 & 3. Subdirectories if (entry.isDirectory() || entry.isSymbolicLink()) { - // Check for package.json with "pi" field first - const packageJsonPath = path.join(entryPath, "package.json"); - if (fs.existsSync(packageJsonPath)) { - const manifest = readPiManifest(packageJsonPath); - if (manifest?.extensions) { - // Load paths declared in manifest (relative to package.json dir) - for (const extPath of manifest.extensions) { - const resolvedExtPath = path.resolve(entryPath, extPath); - if (fs.existsSync(resolvedExtPath)) { - discovered.push(resolvedExtPath); - } - } - continue; // package.json found, don't check for index - } - } - - // Check for index.ts or index.js - const indexTs = path.join(entryPath, "index.ts"); - const indexJs = path.join(entryPath, "index.js"); - if (fs.existsSync(indexTs)) { - discovered.push(indexTs); - } else if (fs.existsSync(indexJs)) { - discovered.push(indexJs); + const entries = resolveExtensionEntries(entryPath); + if (entries) { + discovered.push(...entries); } } } @@ -573,7 +594,18 @@ export async function discoverAndLoadExtensions( addPaths(discoverExtensionsInDir(localExtDir)); // 3. Explicitly configured paths - addPaths(configuredPaths.map((p) => resolvePath(p, cwd))); + for (const p of configuredPaths) { + const resolved = resolvePath(p, cwd); + if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { + const entries = resolveExtensionEntries(resolved); + if (entries) { + addPaths(entries); + continue; + } + } + + addPaths([resolved]); + } return loadExtensions(allPaths, cwd, eventBus); }