fix(cli): expanded marketplace catalog home paths
Resolved persisted home-relative marketplace catalog paths before cache reads and writes, and migrated them to absolute paths on update. Added a regression test covering cwd-relative literal tilde writes. Fixes #8627
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<MarketplaceCatalog> {
|
||||
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 <cloneDir>/marketplace.json
|
||||
// under marketplacesCacheDir/<name>/; parent = <marketplacesCacheDir>/<name>/
|
||||
return path.dirname(entry.catalogPath);
|
||||
return path.dirname(path.resolve(expandTilde(entry.catalogPath)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user