fix: preserve --plugin-dir across cache clears, safe force-reinstall, URL classification
- Store injected --plugin-dir roots in module-level variable; survive cache clears by re-merging on next listClaudePluginRoots() call - Defer old plugin deletion in force-reinstall until new cache succeeds; transient failures no longer leave the plugin uninstalled - Classify HTTP URLs by pathname instead of full string; query params no longer cause .json URLs to be misclassified as git
This commit is contained in:
@@ -746,6 +746,14 @@ export async function listClaudePluginRoots(home: string): Promise<{ roots: Clau
|
||||
}
|
||||
}
|
||||
|
||||
// Merge --plugin-dir roots (highest precedence) on every fresh load
|
||||
if (injectedPluginDirRoots.length > 0) {
|
||||
const injectedIds = new Set(injectedPluginDirRoots.map(r => r.id));
|
||||
const filtered = roots.filter(r => !injectedIds.has(r.id));
|
||||
roots.length = 0;
|
||||
roots.push(...injectedPluginDirRoots, ...filtered);
|
||||
}
|
||||
|
||||
const result = { roots, warnings };
|
||||
pluginRootsCache.set(home, result);
|
||||
return result;
|
||||
@@ -756,7 +764,7 @@ export async function listClaudePluginRoots(home: string): Promise<{ roots: Clau
|
||||
*/
|
||||
export function clearClaudePluginRootsCache(): void {
|
||||
pluginRootsCache.clear();
|
||||
preloadedPluginRoots = [];
|
||||
preloadedPluginRoots = [...injectedPluginDirRoots];
|
||||
}
|
||||
|
||||
// ── Preloaded plugin roots (for sync consumers like LSP config) ─────────────
|
||||
@@ -764,6 +772,7 @@ export function clearClaudePluginRootsCache(): void {
|
||||
// getPreloadedPluginRoots(). Safe degradation: empty array if not warmed.
|
||||
|
||||
let preloadedPluginRoots: ClaudePluginRoot[] = [];
|
||||
let injectedPluginDirRoots: ClaudePluginRoot[] = [];
|
||||
|
||||
/**
|
||||
* Populate the module-level plugin roots cache for sync consumers.
|
||||
@@ -815,6 +824,8 @@ export async function injectPluginDirRoots(home: string, dirs: string[]): Promis
|
||||
|
||||
// --plugin-dir roots have highest precedence: prepend them,
|
||||
// removing any existing entries with the same plugin ID.
|
||||
injectedPluginDirRoots = injected;
|
||||
|
||||
const injectedIds = new Set(injected.map(r => r.id));
|
||||
const filtered = roots.filter(r => !injectedIds.has(r.id));
|
||||
const merged = [...injected, ...filtered];
|
||||
|
||||
@@ -50,7 +50,13 @@ const GITHUB_SHORTHAND_RE = /^[a-z0-9-]+\/[a-z0-9._-]+$/i;
|
||||
export function classifySource(source: string): MarketplaceSourceType {
|
||||
// Rule 1: HTTP(S) URLs — .json suffix → url, everything else → git
|
||||
if (source.startsWith("https://") || source.startsWith("http://")) {
|
||||
return source.endsWith(".json") ? "url" : "git";
|
||||
try {
|
||||
const { pathname } = new URL(source);
|
||||
return pathname.endsWith(".json") ? "url" : "git";
|
||||
} catch {
|
||||
// Malformed URL — treat as git
|
||||
return "git";
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 2: SCP-style SSH git URLs
|
||||
|
||||
@@ -200,17 +200,8 @@ export class MarketplaceManager {
|
||||
// 3. Check if already installed
|
||||
const instReg = await readInstalledPluginsRegistry(this.#opts.installedRegistryPath);
|
||||
const existing = getInstalledPlugin(instReg, pluginId);
|
||||
if (existing && existing.length > 0) {
|
||||
if (!force) {
|
||||
throw new Error(`Plugin "${pluginId}" is already installed. Use force option to reinstall.`);
|
||||
}
|
||||
// Remove all cached entries for this plugin (there may be multiple versions)
|
||||
for (const entry of existing) {
|
||||
await fs.rm(entry.installPath, { recursive: true, force: true });
|
||||
}
|
||||
const prunedReg = removeInstalledPlugin(instReg, pluginId);
|
||||
await writeInstalledPluginsRegistry(this.#opts.installedRegistryPath, prunedReg);
|
||||
this.#opts.clearPluginRootsCache?.();
|
||||
if (existing && existing.length > 0 && !force) {
|
||||
throw new Error(`Plugin "${pluginId}" is already installed. Use force option to reinstall.`);
|
||||
}
|
||||
|
||||
// 4. Resolve source path.
|
||||
@@ -245,6 +236,21 @@ export class MarketplaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Only now clean up old entries — new cache succeeded, so it is safe to remove old ones.
|
||||
if (existing && existing.length > 0) {
|
||||
for (const entry of existing) {
|
||||
// Skip if the new cache resolved to the same path (same version reinstall).
|
||||
if (entry.installPath !== cachePath) {
|
||||
await fs.rm(entry.installPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
const prunedReg = removeInstalledPlugin(
|
||||
await readInstalledPluginsRegistry(this.#opts.installedRegistryPath),
|
||||
pluginId,
|
||||
);
|
||||
await writeInstalledPluginsRegistry(this.#opts.installedRegistryPath, prunedReg);
|
||||
}
|
||||
|
||||
// 6. Build and register the entry
|
||||
const now = new Date().toISOString();
|
||||
const installedEntry: InstalledPluginEntry = {
|
||||
|
||||
Reference in New Issue
Block a user