diff --git a/packages/dotagents/src/plugins/store.test.ts b/packages/dotagents/src/plugins/store.test.ts index 47100dc..7dd591c 100644 --- a/packages/dotagents/src/plugins/store.test.ts +++ b/packages/dotagents/src/plugins/store.test.ts @@ -464,6 +464,54 @@ describe("plugin store", () => { } }); + it("records every native fallback manifest when no root plugin.json exists", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "dotagents-plugin-dual-native-")); + try { + const sourceRoot = join(projectRoot, "source"); + const pluginsDir = join(projectRoot, "installed"); + await mkdir(join(sourceRoot, ".codex-plugin"), { recursive: true }); + await mkdir(join(sourceRoot, ".claude-plugin"), { recursive: true }); + await mkdir(pluginsDir, { recursive: true }); + await writeFile( + join(sourceRoot, ".codex-plugin", "plugin.json"), + JSON.stringify({ name: "dual-tools" }), + ); + await writeFile( + join(sourceRoot, ".claude-plugin", "plugin.json"), + JSON.stringify({ name: "dual-tools" }), + ); + + const candidates = await discoverPlugins(sourceRoot); + expect(candidates).toHaveLength(1); + expect(candidates[0]!.nativeSource).toBe("codex"); + expect(candidates[0]!.authoredNativeInterfaces.codex?.fallback).toBe(true); + expect(candidates[0]!.authoredNativeInterfaces.claude?.fallback).toBe(true); + + const resolved = await resolvePlugin( + { name: "dual-tools", source: "path:source" }, + { stateDir: join(projectRoot, "state"), projectRoot }, + ); + const prepared = preparePluginForTargets(resolved.plugin, ["claude", "codex"]); + const installed = await installPluginBundle(pluginsDir, { + ...resolved, + plugin: prepared, + }); + expect(installed.authoredNativeInterfaces?.claude?.fallback).toBe(true); + + const reloaded = await loadInstalledPlugins( + pluginsDir, + [{ name: "dual-tools", source: "path:source" }], + "dotagents install", + ["claude", "codex"], + ); + expect(reloaded.issues).toEqual([]); + expect(reloaded.plugins).toHaveLength(1); + expect(reloaded.plugins[0]!.authoredNativeInterfaces?.claude?.fallback).toBe(true); + } finally { + await rm(projectRoot, { recursive: true, force: true }); + } + }); + it("rejects a .github Copilot manifest when a native fallback was imported", async () => { const projectRoot = await mkdtemp(join(tmpdir(), "dotagents-plugin-copilot-precedence-")); try { diff --git a/packages/dotagents/src/plugins/store.ts b/packages/dotagents/src/plugins/store.ts index 5e75dfd..d2e355e 100644 --- a/packages/dotagents/src/plugins/store.ts +++ b/packages/dotagents/src/plugins/store.ts @@ -905,7 +905,10 @@ async function loadPluginInterfaces( throw new Error("Installed plugin bundle is missing plugin.json. Reinstall the plugin."); } + let primary: { manifest: PluginManifest; nativeSource?: NativePluginSource } | undefined; + const authoredNativeInterfaces: AuthoredNativePluginInterfaces = {}; for (const candidate of FALLBACK_MANIFEST_PATHS) { + if (primary && !candidate.nativeSource) {continue;} const filePath = join(pluginDir, candidate.path); if (!existsSync(filePath)) {continue;} const value = await readJson(filePath); @@ -915,7 +918,7 @@ async function loadPluginInterfaces( } catch (err) { throw pluginManifestError(err, value); } - const authoredNativeInterfaces: AuthoredNativePluginInterfaces = {}; + primary ??= { manifest, nativeSource: candidate.nativeSource }; if (candidate.nativeSource) { authoredNativeInterfaces[candidate.nativeSource] = { path: candidate.path, @@ -923,13 +926,13 @@ async function loadPluginInterfaces( manifest, }; } - return { - manifest, - authoredNativeInterfaces, - nativeSource: candidate.nativeSource, - }; } - return null; + if (!primary) {return null;} + return { + manifest: primary.manifest, + authoredNativeInterfaces, + nativeSource: primary.nativeSource, + }; } async function loadAuthoredNativeInterfaces( @@ -1135,10 +1138,7 @@ async function readInstalledPluginProvenance(pluginDir: string): Promise<{ }> { const fallbackSources = await readNativeFallbackSources(pluginDir); const nativeSource = await readNativeSourceMarker(pluginDir); - if ( - fallbackSources && nativeSource && - (fallbackSources.size !== 1 || !fallbackSources.has(nativeSource)) - ) { + if (fallbackSources && nativeSource && !fallbackSources.has(nativeSource)) { throw new Error("Installed plugin has conflicting native interface provenance. Reinstall the plugin."); } return { fallbackSources, nativeSource };