diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5597d222b..7c3521432 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Expanded home-relative marketplace catalog paths before cache access, preventing updates from writing into a literal `~` directory under the process working directory ([#8627](https://github.com/can1357/oh-my-pi/issues/8627)). + ## [17.3.4] - 2026-08-14 ### Changed diff --git a/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts b/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts index 79002fd8c..72733e9f3 100644 --- a/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts +++ b/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts @@ -11,6 +11,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { isEnoent, logger, pathIsWithin } from "@oh-my-pi/pi-utils"; +import { expandTilde } from "../../../tools/path-utils"; import { normalizePluginRuntimeConfig } from "../runtime-config"; import type { PluginRuntimeConfig } from "../types"; @@ -107,12 +108,11 @@ export class MarketplaceManager { } const sourceType = classifySource(source); - const normalizedSource = - sourceType === "local" - ? path.resolve(source.startsWith("~/") ? path.join(os.homedir(), source.slice(2)) : source) - : source; + const normalizedSource = sourceType === "local" ? path.resolve(expandTilde(source)) : source; - const catalogPath = path.join(this.#opts.marketplacesCacheDir, catalog.name, "marketplace.json"); + const catalogPath = path.resolve( + expandTilde(path.join(this.#opts.marketplacesCacheDir, catalog.name, "marketplace.json")), + ); // Persist the fetched catalog so subsequent reads don't require re-fetching. await Bun.write(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); @@ -174,11 +174,13 @@ export class MarketplaceManager { await promoteCloneToCache(clonePath, this.#opts.marketplacesCacheDir, catalog.name); } - // Overwrite cached catalog - await Bun.write(existing.catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); + // Overwrite the cached catalog and migrate legacy home-relative registry entries. + const catalogPath = path.resolve(expandTilde(existing.catalogPath)); + await Bun.write(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); const updatedEntry: MarketplaceRegistryEntry = { ...existing, + catalogPath, updatedAt: new Date().toISOString(), }; @@ -893,14 +895,13 @@ export class MarketplaceManager { } async #readCatalog(entry: MarketplaceRegistryEntry): Promise { + const catalogPath = path.resolve(expandTilde(entry.catalogPath)); try { - const content = await Bun.file(entry.catalogPath).text(); - return parseMarketplaceCatalog(content, entry.catalogPath); + const content = await Bun.file(catalogPath).text(); + return parseMarketplaceCatalog(content, catalogPath); } catch (err) { if (isEnoent(err)) { - throw new Error( - `Marketplace catalog not found at ${entry.catalogPath}. Try: /marketplace update ${entry.name}`, - ); + throw new Error(`Marketplace catalog not found at ${catalogPath}. Try: /marketplace update ${entry.name}`); } throw err; } @@ -918,14 +919,10 @@ export class MarketplaceManager { */ #resolveMarketplaceRoot(entry: MarketplaceRegistryEntry): string { if (entry.sourceType === "local") { - // expandHome already happened in fetcher; resolve to ensure absolute. - const expanded = entry.sourceUri.startsWith("~/") - ? path.join(os.homedir(), entry.sourceUri.slice(2)) - : entry.sourceUri; - return path.resolve(expanded); + return path.resolve(expandTilde(entry.sourceUri)); } // For git/github/url sources, the catalog lives at /marketplace.json // under marketplacesCacheDir//; parent = // - return path.dirname(entry.catalogPath); + return path.dirname(path.resolve(expandTilde(entry.catalogPath))); } } diff --git a/packages/coding-agent/test/marketplace/manager.test.ts b/packages/coding-agent/test/marketplace/manager.test.ts index 1097ee250..1f608832b 100644 --- a/packages/coding-agent/test/marketplace/manager.test.ts +++ b/packages/coding-agent/test/marketplace/manager.test.ts @@ -9,6 +9,8 @@ import { PluginManager } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/m import { MarketplaceManager, readInstalledPluginsRegistry, + readMarketplacesRegistry, + writeMarketplacesRegistry, } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/marketplace"; import * as piUtils from "@oh-my-pi/pi-utils"; import { removeSyncWithRetries } from "@oh-my-pi/pi-utils"; @@ -168,6 +170,34 @@ describe("MarketplaceManager", () => { expect(new Date(updated.updatedAt) >= new Date(added.addedAt)).toBe(true); }); + it("updateMarketplace expands a home-relative catalog path", async () => { + const fakeHome = fs.mkdtempSync(path.join(ctx.tmpDir, "home-")); + const homedirSpy = spyOn(os, "homedir").mockReturnValue(fakeHome); + const registryPath = path.join(ctx.tmpDir, "marketplaces.json"); + const originalCwd = process.cwd(); + const cwd = path.join(ctx.tmpDir, "cwd"); + + try { + const added = await ctx.manager.addMarketplace(FIXTURE_DIR); + const catalogPath = "~/.omp/plugins/cache/marketplaces/test-marketplace/marketplace.json"; + const registry = await readMarketplacesRegistry(registryPath); + await writeMarketplacesRegistry(registryPath, { + ...registry, + marketplaces: [{ ...added, catalogPath }], + }); + fs.mkdirSync(cwd); + process.chdir(cwd); + + await ctx.manager.updateMarketplace("test-marketplace"); + + expect(fs.existsSync(path.join(fakeHome, catalogPath.slice(2)))).toBe(true); + expect(fs.existsSync(path.join(cwd, catalogPath))).toBe(false); + } finally { + process.chdir(originalCwd); + homedirSpy.mockRestore(); + } + }); + // ── Plugin discovery ─────────────────────────────────────────────────── it("listAvailablePlugins → returns catalog entries", async () => {