From dcbcecfb85e722156e4a9c698ded3972c0da9689 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 19 Mar 2026 09:38:30 -0700 Subject: [PATCH] fix(ci): resolve Claude marketplace shortcuts from OS home --- src/infra/home-dir.test.ts | 30 +++++++++++++++++++++ src/infra/home-dir.ts | 46 ++++++++++++++++++++++++++++++--- src/plugins/marketplace.test.ts | 4 ++- src/plugins/marketplace.ts | 3 ++- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/infra/home-dir.test.ts b/src/infra/home-dir.test.ts index 9faeda1dee5..2382b56eaac 100644 --- a/src/infra/home-dir.test.ts +++ b/src/infra/home-dir.test.ts @@ -4,6 +4,8 @@ import { expandHomePrefix, resolveEffectiveHomeDir, resolveHomeRelativePath, + resolveOsHomeDir, + resolveOsHomeRelativePath, resolveRequiredHomeDir, } from "./home-dir.js"; @@ -95,6 +97,21 @@ describe("resolveRequiredHomeDir", () => { }); }); +describe("resolveOsHomeDir", () => { + it("ignores OPENCLAW_HOME and uses HOME", () => { + expect( + resolveOsHomeDir( + { + OPENCLAW_HOME: "/srv/openclaw-home", + HOME: "/home/alice", + USERPROFILE: "C:/Users/alice", + } as NodeJS.ProcessEnv, + () => "/fallback", + ), + ).toBe(path.resolve("/home/alice")); + }); +}); + describe("expandHomePrefix", () => { it.each([ { @@ -158,3 +175,16 @@ describe("resolveHomeRelativePath", () => { ).toBe(path.resolve(process.cwd())); }); }); + +describe("resolveOsHomeRelativePath", () => { + it("expands tilde paths using the OS home instead of OPENCLAW_HOME", () => { + expect( + resolveOsHomeRelativePath("~/docs", { + env: { + OPENCLAW_HOME: "/srv/openclaw-home", + HOME: "/home/alice", + } as NodeJS.ProcessEnv, + }), + ).toBe(path.resolve("/home/alice/docs")); + }); +}); diff --git a/src/infra/home-dir.ts b/src/infra/home-dir.ts index 650cf0cadac..956eeebb278 100644 --- a/src/infra/home-dir.ts +++ b/src/infra/home-dir.ts @@ -14,12 +14,19 @@ export function resolveEffectiveHomeDir( return raw ? path.resolve(raw) : undefined; } +export function resolveOsHomeDir( + env: NodeJS.ProcessEnv = process.env, + homedir: () => string = os.homedir, +): string | undefined { + const raw = resolveRawOsHomeDir(env, homedir); + return raw ? path.resolve(raw) : undefined; +} + function resolveRawHomeDir(env: NodeJS.ProcessEnv, homedir: () => string): string | undefined { const explicitHome = normalize(env.OPENCLAW_HOME); if (explicitHome) { if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) { - const fallbackHome = - normalize(env.HOME) ?? normalize(env.USERPROFILE) ?? normalizeSafe(homedir); + const fallbackHome = resolveRawOsHomeDir(env, homedir); if (fallbackHome) { return explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome); } @@ -28,16 +35,18 @@ function resolveRawHomeDir(env: NodeJS.ProcessEnv, homedir: () => string): strin return explicitHome; } + return resolveRawOsHomeDir(env, homedir); +} + +function resolveRawOsHomeDir(env: NodeJS.ProcessEnv, homedir: () => string): string | undefined { const envHome = normalize(env.HOME); if (envHome) { return envHome; } - const userProfile = normalize(env.USERPROFILE); if (userProfile) { return userProfile; } - return normalizeSafe(homedir); } @@ -56,6 +65,13 @@ export function resolveRequiredHomeDir( return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd()); } +export function resolveRequiredOsHomeDir( + env: NodeJS.ProcessEnv = process.env, + homedir: () => string = os.homedir, +): string { + return resolveOsHomeDir(env, homedir) ?? path.resolve(process.cwd()); +} + export function expandHomePrefix( input: string, opts?: { @@ -97,3 +113,25 @@ export function resolveHomeRelativePath( } return path.resolve(trimmed); } + +export function resolveOsHomeRelativePath( + input: string, + opts?: { + env?: NodeJS.ProcessEnv; + homedir?: () => string; + }, +): string { + const trimmed = input.trim(); + if (!trimmed) { + return trimmed; + } + if (trimmed.startsWith("~")) { + const expanded = expandHomePrefix(trimmed, { + home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir), + env: opts?.env, + homedir: opts?.homedir, + }); + return path.resolve(expanded); + } + return path.resolve(trimmed); +} diff --git a/src/plugins/marketplace.test.ts b/src/plugins/marketplace.test.ts index 92918e256d4..6ae2b010556 100644 --- a/src/plugins/marketplace.test.ts +++ b/src/plugins/marketplace.test.ts @@ -111,7 +111,9 @@ describe("marketplace plugins", () => { it("resolves Claude-style plugin@marketplace shortcuts from known_marketplaces.json", async () => { await withTempDir(async (homeDir) => { + const openClawHome = path.join(homeDir, "openclaw-home"); await fs.mkdir(path.join(homeDir, ".claude", "plugins"), { recursive: true }); + await fs.mkdir(openClawHome, { recursive: true }); await fs.writeFile( path.join(homeDir, ".claude", "plugins", "known_marketplaces.json"), JSON.stringify({ @@ -127,7 +129,7 @@ describe("marketplace plugins", () => { const { resolveMarketplaceInstallShortcut } = await import("./marketplace.js"); const shortcut = await withEnvAsync( - { HOME: homeDir }, + { HOME: homeDir, OPENCLAW_HOME: openClawHome }, async () => await resolveMarketplaceInstallShortcut("superpowers@claude-plugins-official"), ); diff --git a/src/plugins/marketplace.ts b/src/plugins/marketplace.ts index 4999c3c8828..24d2fae8ba1 100644 --- a/src/plugins/marketplace.ts +++ b/src/plugins/marketplace.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { resolveArchiveKind } from "../infra/archive.js"; +import { resolveOsHomeRelativePath } from "../infra/home-dir.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { resolveUserPath } from "../utils.js"; import { installPluginFromPath, type InstallPluginResult } from "./install.js"; @@ -299,7 +300,7 @@ async function pathExists(target: string): Promise { } async function readClaudeKnownMarketplaces(): Promise> { - const knownPath = resolveUserPath(CLAUDE_KNOWN_MARKETPLACES_PATH); + const knownPath = resolveOsHomeRelativePath(CLAUDE_KNOWN_MARKETPLACES_PATH); if (!(await pathExists(knownPath))) { return {}; }