Merge PR #8440: fix(discovery): honor Claude config directory (@roboomp)
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Fixed
|
||||
|
||||
- Fixed Claude Code user discovery ignoring `CLAUDE_CONFIG_DIR` for configuration, plugins, MCP servers, and imported sessions ([#8436](https://github.com/can1357/oh-my-pi/issues/8436)).
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { CONFIG_DIR_NAME, getConfigAgentDirName, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { resolveClaudePaths } from "./config/claude-paths";
|
||||
import { expandTilde } from "./tools/path-utils";
|
||||
|
||||
export * from "./config/config-file";
|
||||
@@ -76,12 +77,12 @@ export function getChangelogPath(): string | undefined {
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Config directory bases in priority order (highest first).
|
||||
* User-level: ~/.omp/agent, ~/.claude, ~/.codex, ~/.gemini
|
||||
* User-level: ~/.omp/agent, Claude's active config directory, ~/.codex, ~/.gemini
|
||||
* Project-level: .omp, .claude, .codex, .gemini
|
||||
*/
|
||||
const USER_CONFIG_BASES = priorityList.map(({ dir, globalAgentDir }) => ({
|
||||
base: () => path.join(os.homedir(), globalAgentDir ? globalAgentDir() : dir),
|
||||
base: () =>
|
||||
dir === ".claude" ? resolveClaudePaths().configDir : path.join(os.homedir(), globalAgentDir?.() ?? dir),
|
||||
name: dir,
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
/** Paths to Claude Code's user data and configuration file. */
|
||||
export interface ClaudePaths {
|
||||
configDir: string;
|
||||
configFile: string;
|
||||
}
|
||||
|
||||
/** Resolves Claude Code's user paths, honoring `CLAUDE_CONFIG_DIR`. */
|
||||
export function resolveClaudePaths(home: string = os.homedir()): ClaudePaths {
|
||||
const override = process.env.CLAUDE_CONFIG_DIR?.trim();
|
||||
if (override) {
|
||||
const configDir = path.resolve(override);
|
||||
return { configDir, configFile: path.join(configDir, ".claude.json") };
|
||||
}
|
||||
return { configDir: path.join(home, ".claude"), configFile: path.join(home, ".claude.json") };
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
|
||||
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
||||
import { type CustomTool, toolCapability } from "../capability/tool";
|
||||
import type { LoadContext, LoadResult } from "../capability/types";
|
||||
import { resolveClaudePaths } from "../config/claude-paths";
|
||||
import { settings } from "../config/settings";
|
||||
import {
|
||||
calculateDepth,
|
||||
@@ -34,11 +35,10 @@ const DISPLAY_NAME = "Claude Code";
|
||||
const PRIORITY = 80;
|
||||
const CONFIG_DIR = ".claude";
|
||||
|
||||
/**
|
||||
* Get user-level .claude path.
|
||||
*/
|
||||
/** Get the active user-level Claude Code directory. */
|
||||
function getUserClaude(ctx: LoadContext): string {
|
||||
return path.join(ctx.home, CONFIG_DIR);
|
||||
const { configDir } = resolveClaudePaths(ctx.home);
|
||||
return configDir;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +60,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
||||
const items: MCPServer[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const userBase = getUserClaude(ctx);
|
||||
const userClaudeJson = path.join(ctx.home, ".claude.json");
|
||||
const { configDir: userBase, configFile: userClaudeJson } = resolveClaudePaths(ctx.home);
|
||||
const userMcpJson = path.join(userBase, "mcp.json");
|
||||
|
||||
const projectBase = path.join(ctx.cwd, CONFIG_DIR);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../ca
|
||||
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
||||
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
||||
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
||||
import { resolveClaudePaths } from "../config/claude-paths";
|
||||
import type { MCPRequestIdFormat } from "../mcp/types";
|
||||
import { type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../thinking";
|
||||
import { normalizeToolNames } from "../tools/builtin-names";
|
||||
@@ -90,10 +91,9 @@ export type SourceId = keyof typeof SOURCE_PATHS;
|
||||
*/
|
||||
export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string): string | null {
|
||||
// Native user config is profile-scoped via getAgentDir() (the active profile's
|
||||
// agent dir), matching builtin.ts and getMCPConfigPath("user"). External tools
|
||||
// (~/.claude, ~/.gemini, …) are intentionally not profile-scoped, so they keep
|
||||
// resolving against ctx.home below.
|
||||
// agent dir), matching builtin.ts and getMCPConfigPath("user").
|
||||
if (source === "native") return path.join(getAgentDir(), subpath);
|
||||
if (source === "claude") return path.join(resolveClaudePaths(ctx.home).configDir, subpath);
|
||||
const paths = SOURCE_PATHS[source];
|
||||
if (!paths.userAgent) return null;
|
||||
return path.join(ctx.home, paths.userAgent, subpath);
|
||||
@@ -903,20 +903,21 @@ export function registerPluginCacheInvalidator(invalidator: () => void): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* List all installed Claude Code plugin roots from the plugin cache.
|
||||
* Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
|
||||
* and optionally the nearest project-scoped registry resolved from `cwd`.
|
||||
* List all installed Claude Code plugin roots from its active plugin cache and
|
||||
* ~/.omp/plugins/installed_plugins.json, plus the nearest project registry when present.
|
||||
*
|
||||
* Results are cached per home, project registry, and canonical active project.
|
||||
* Results are cached per Claude and OMP config directories, project registry, and canonical active project.
|
||||
*/
|
||||
export async function listClaudePluginRoots(
|
||||
home: string,
|
||||
cwd?: string,
|
||||
): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
|
||||
const claudeConfigDir = resolveClaudePaths(home).configDir;
|
||||
const ompRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
|
||||
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
||||
const projectRoot = resolvedProjectPath ? path.dirname(path.dirname(path.dirname(resolvedProjectPath))) : cwd;
|
||||
const activeClaudeProjectPath = projectRoot ? await canonicalClaudeProjectPath(projectRoot) : null;
|
||||
const cacheKey = `${home}:${resolvedProjectPath ?? ""}:${activeClaudeProjectPath ?? ""}`;
|
||||
const cacheKey = `${claudeConfigDir}:${ompRegistryPath}:${resolvedProjectPath ?? ""}:${activeClaudeProjectPath ?? ""}`;
|
||||
const cached = pluginRootsCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
@@ -926,7 +927,7 @@ export async function listClaudePluginRoots(
|
||||
const canonicalClaudeProjectPaths = new Map<string, string | null>();
|
||||
|
||||
// ── Claude Code registry ──────────────────────────────────────────────────
|
||||
const registryPath = path.join(home, ".claude", "plugins", "installed_plugins.json");
|
||||
const registryPath = path.join(claudeConfigDir, "plugins", "installed_plugins.json");
|
||||
const content = await readFile(registryPath);
|
||||
|
||||
if (content) {
|
||||
@@ -983,7 +984,7 @@ export async function listClaudePluginRoots(
|
||||
// In production `home` is `os.homedir()`, so `getPluginsDir(home)` resolves to the
|
||||
// same XDG-aware path the marketplace writer uses (reads and writes always agree).
|
||||
// Tests pass a temp dir, which short-circuits the resolver for deterministic isolation.
|
||||
const ompRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
|
||||
// Computed before the cache lookup because isolated SDK homes select distinct OMP registries.
|
||||
const ompContent = await readFile(ompRegistryPath);
|
||||
if (ompContent) {
|
||||
const ompRegistry = parseClaudePluginsRegistry(ompContent);
|
||||
@@ -1107,7 +1108,7 @@ export function clearClaudePluginRootsCache(): void {
|
||||
* installing/uninstalling/enabling/disabling plugins.
|
||||
*/
|
||||
export function clearPluginRootsAndCaches(extraPaths?: readonly string[]): void {
|
||||
invalidateFsCache(path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json"));
|
||||
invalidateFsCache(path.join(resolveClaudePaths().configDir, "plugins", "installed_plugins.json"));
|
||||
invalidateFsCache(path.join(getPluginsDir(), "installed_plugins.json"));
|
||||
for (const p of extraPaths ?? []) invalidateFsCache(p);
|
||||
clearClaudePluginRootsCache();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type * as fsTypes from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
@@ -13,6 +12,7 @@ import type {
|
||||
UserMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { isRecord } from "@oh-my-pi/pi-utils";
|
||||
import { resolveClaudePaths } from "../config/claude-paths";
|
||||
import { collectForeignJsonRecords, type ForeignJsonRecord, readForeignJsonRecords } from "./foreign-session-jsonl";
|
||||
import type { ForeignSessionInfo, ForeignSessionStore } from "./foreign-session-store";
|
||||
import type { ModelChangeEntry, SessionMessageEntry } from "./session-entries";
|
||||
@@ -99,7 +99,8 @@ async function readHistoryIndex(file: string): Promise<Map<string, ClaudeHistory
|
||||
}
|
||||
|
||||
async function readRegisteredProjects(root: string): Promise<string[]> {
|
||||
const config = path.join(path.dirname(root), ".claude.json");
|
||||
const { configDir, configFile } = resolveClaudePaths();
|
||||
const config = root === configDir ? configFile : path.join(path.dirname(root), ".claude.json");
|
||||
try {
|
||||
const parsed: unknown = await Bun.file(config).json();
|
||||
if (!isRecord(parsed) || !isRecord(parsed.projects)) return [];
|
||||
@@ -306,7 +307,7 @@ export class ClaudeSessionStore implements ForeignSessionStore {
|
||||
readonly #root: string;
|
||||
|
||||
/** Creates a store rooted at Claude's data directory, or at a fixture root when supplied. */
|
||||
constructor(root: string = path.join(os.homedir(), ".claude")) {
|
||||
constructor(root: string = resolveClaudePaths().configDir) {
|
||||
this.#root = path.resolve(root);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,14 @@ describe("Claude Code slash command discovery", () => {
|
||||
let home = "";
|
||||
let project = "";
|
||||
let originalHome: string | undefined;
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
clearFsCache();
|
||||
resetSettingsForTest();
|
||||
originalHome = process.env.HOME;
|
||||
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
root = await fs.mkdtemp(path.join(os.tmpdir(), "omp-claude-commands-"));
|
||||
home = path.join(root, "home");
|
||||
project = path.join(root, "project");
|
||||
@@ -40,6 +43,11 @@ describe("Claude Code slash command discovery", () => {
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
if (originalClaudeConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
|
||||
}
|
||||
await removeWithRetries(root);
|
||||
});
|
||||
|
||||
@@ -61,6 +69,24 @@ describe("Claude Code slash command discovery", () => {
|
||||
expect(names).toContain("audit");
|
||||
expect(names).toContain("team:audit");
|
||||
});
|
||||
|
||||
test("loads user commands from CLAUDE_CONFIG_DIR instead of the legacy home", async () => {
|
||||
const relocated = path.join(root, "relocated-claude");
|
||||
process.env.CLAUDE_CONFIG_DIR = relocated;
|
||||
await writeFile(path.join(home, ".claude", "commands", "stale.md"), "Stale prompt\n");
|
||||
await writeFile(path.join(relocated, "commands", "active.md"), "Active prompt\n");
|
||||
|
||||
const result = await loadCapability<SlashCommand>(slashCommandCapability.id, {
|
||||
cwd: project,
|
||||
providers: ["claude"],
|
||||
});
|
||||
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(result.items.find(command => command.name === "active")?.path).toBe(
|
||||
path.join(relocated, "commands", "active.md"),
|
||||
);
|
||||
expect(result.items.some(command => command.name === "stale")).toBe(false);
|
||||
});
|
||||
test("keeps root commands ahead of nested basename duplicates", async () => {
|
||||
const rootApply = path.join(project, ".claude", "commands", "apply.md");
|
||||
const nestedApply = path.join(project, ".claude", "commands", "agent", "apply.md");
|
||||
|
||||
@@ -76,6 +76,7 @@ describe("listClaudePluginRoots", () => {
|
||||
let originalAgentDirEnv: string | undefined;
|
||||
let originalOmpProfileEnv: string | undefined;
|
||||
let originalPiProfileEnv: string | undefined;
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
clearClaudePluginRootsCache();
|
||||
@@ -84,6 +85,8 @@ describe("listClaudePluginRoots", () => {
|
||||
originalAgentDirEnv = process.env.PI_CODING_AGENT_DIR;
|
||||
originalOmpProfileEnv = process.env.OMP_PROFILE;
|
||||
originalPiProfileEnv = process.env.PI_PROFILE;
|
||||
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-plugins-test-"));
|
||||
testAgentDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-plugins-test-agent-"));
|
||||
process.env.HOME = tempDir;
|
||||
@@ -103,6 +106,7 @@ describe("listClaudePluginRoots", () => {
|
||||
restoreEnvValue("OMP_PROFILE", originalOmpProfileEnv);
|
||||
restoreEnvValue("PI_PROFILE", originalPiProfileEnv);
|
||||
restoreEnvValue("PI_CODING_AGENT_DIR", originalAgentDirEnv);
|
||||
restoreEnvValue("CLAUDE_CONFIG_DIR", originalClaudeConfigDir);
|
||||
__resetDirsFromEnvForTests();
|
||||
await removeWithRetries(tempDir);
|
||||
await removeWithRetries(testAgentDir);
|
||||
@@ -147,6 +151,41 @@ describe("listClaudePluginRoots", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("reads the user plugin registry from CLAUDE_CONFIG_DIR", async () => {
|
||||
const relocated = path.join(tempDir, "relocated-claude");
|
||||
const pluginsDir = path.join(relocated, "plugins");
|
||||
process.env.CLAUDE_CONFIG_DIR = relocated;
|
||||
await fs.mkdir(pluginsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginsDir, "installed_plugins.json"),
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
plugins: {
|
||||
"relocated@market": [
|
||||
{
|
||||
scope: "user",
|
||||
installPath: "/path/to/relocated",
|
||||
version: "1.0.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await listClaudePluginRoots(tempDir);
|
||||
|
||||
expect(result.roots).toEqual([
|
||||
{
|
||||
id: "relocated@market",
|
||||
marketplace: "market",
|
||||
plugin: "relocated",
|
||||
version: "1.0.0",
|
||||
path: "/path/to/relocated",
|
||||
scope: "user",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("isolates local plugins to their canonical project", async () => {
|
||||
const pluginsDir = path.join(tempDir, ".claude", "plugins");
|
||||
const projectA = path.join(tempDir, "project-a");
|
||||
@@ -355,6 +394,41 @@ describe("listClaudePluginRoots", () => {
|
||||
expect(result3.roots).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("isolates cached OMP plugin roots by home when Claude config is shared", async () => {
|
||||
const sharedClaudeConfig = path.join(tempDir, "shared-claude");
|
||||
const firstHome = path.join(tempDir, "first-home");
|
||||
const secondHome = path.join(tempDir, "second-home");
|
||||
process.env.CLAUDE_CONFIG_DIR = sharedClaudeConfig;
|
||||
for (const [home, pluginId] of [
|
||||
[firstHome, "first@market"],
|
||||
[secondHome, "second@market"],
|
||||
] as const) {
|
||||
const pluginsDir = path.join(home, ".omp", "plugins");
|
||||
await fs.mkdir(pluginsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginsDir, "installed_plugins.json"),
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
plugins: {
|
||||
[pluginId]: [
|
||||
{
|
||||
scope: "user",
|
||||
installPath: `/path/to/${pluginId.split("@")[0]}`,
|
||||
version: "1.0.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const first = await listClaudePluginRoots(firstHome);
|
||||
const second = await listClaudePluginRoots(secondHome);
|
||||
|
||||
expect(first.roots.map(root => root.id)).toEqual(["first@market"]);
|
||||
expect(second.roots.map(root => root.id)).toEqual(["second@market"]);
|
||||
});
|
||||
|
||||
test("defaults scope to user when not specified", async () => {
|
||||
const pluginsDir = path.join(tempDir, ".claude", "plugins");
|
||||
await fs.mkdir(pluginsDir, { recursive: true });
|
||||
@@ -1305,15 +1379,19 @@ describe("listClaudePluginRoots", () => {
|
||||
|
||||
describe("discoverAgents plugin precedence", () => {
|
||||
let tempDir: string;
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
clearClaudePluginRootsCache();
|
||||
clearFsCache();
|
||||
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-plugins-precedence-test-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
clearClaudePluginRootsCache();
|
||||
restoreEnvValue("CLAUDE_CONFIG_DIR", originalClaudeConfigDir);
|
||||
await removeWithRetries(tempDir);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { LoadContext } from "@oh-my-pi/pi-coding-agent/capability/types";
|
||||
import { getConfigDirs } from "@oh-my-pi/pi-coding-agent/config";
|
||||
import { resolveClaudePaths } from "@oh-my-pi/pi-coding-agent/config/claude-paths";
|
||||
import { getUserPath } from "@oh-my-pi/pi-coding-agent/discovery/helpers";
|
||||
import { getAgentDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
@@ -37,3 +38,41 @@ describe("PI_CONFIG_DIR", () => {
|
||||
expect(result[0]).toEqual({ path: expected, source: ".omp", level: "user" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLAUDE_CONFIG_DIR", () => {
|
||||
const original = process.env.CLAUDE_CONFIG_DIR;
|
||||
afterEach(() => {
|
||||
if (original === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = original;
|
||||
}
|
||||
});
|
||||
|
||||
test("relocates Claude user discovery and .claude.json together", () => {
|
||||
process.env.CLAUDE_CONFIG_DIR = "./fixtures/claude-home";
|
||||
const expectedRoot = path.resolve("./fixtures/claude-home");
|
||||
const ctx: LoadContext = {
|
||||
cwd: "/work/project",
|
||||
home: "/home/tester",
|
||||
repoRoot: null,
|
||||
};
|
||||
|
||||
expect(resolveClaudePaths(ctx.home)).toEqual({
|
||||
configDir: expectedRoot,
|
||||
configFile: path.join(expectedRoot, ".claude.json"),
|
||||
});
|
||||
expect(getUserPath(ctx, "claude", "commands")).toBe(path.join(expectedRoot, "commands"));
|
||||
expect(
|
||||
getConfigDirs("commands", { user: true, project: false }).find(entry => entry.source === ".claude"),
|
||||
).toEqual({ path: path.join(expectedRoot, "commands"), source: ".claude", level: "user" });
|
||||
});
|
||||
|
||||
test("keeps the legacy split paths when the override is unset", () => {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
expect(resolveClaudePaths("/home/tester")).toEqual({
|
||||
configDir: path.join("/home/tester", ".claude"),
|
||||
configFile: path.join("/home/tester", ".claude.json"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -12,12 +12,21 @@ import { SessionManager } from "../src/session/session-manager";
|
||||
import { FileSessionStorage } from "../src/session/session-storage";
|
||||
|
||||
let tempRoot: string;
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-foreign-sessions-"));
|
||||
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
if (originalClaudeConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
|
||||
}
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -130,6 +139,30 @@ describe("ClaudeSessionStore", () => {
|
||||
expect(sessions[0]?.firstMessage).toBe("Legacy prompt");
|
||||
expect(sessions[0]?.created.toISOString()).toBe("2025-01-01T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it("defaults to CLAUDE_CONFIG_DIR and reads its colocated project registry", async () => {
|
||||
const root = path.join(tempRoot, "relocated-claude");
|
||||
const cwd = path.join(tempRoot, "project-with-hyphen");
|
||||
const id = "22222222-2222-4222-8222-333333333333";
|
||||
const encoded = cwd.replaceAll(path.sep, "-");
|
||||
process.env.CLAUDE_CONFIG_DIR = root;
|
||||
await Bun.write(path.join(root, ".claude.json"), JSON.stringify({ projects: { [cwd]: {} } }));
|
||||
await writeJsonl(path.join(root, "projects", encoded, `${id}.jsonl`), [
|
||||
{
|
||||
type: "user",
|
||||
uuid: "relocated-user",
|
||||
parentUuid: null,
|
||||
timestamp: "2025-01-01T00:00:00.000Z",
|
||||
message: { content: "Relocated prompt" },
|
||||
},
|
||||
]);
|
||||
|
||||
const sessions = await new ClaudeSessionStore().list();
|
||||
|
||||
expect(sessions).toHaveLength(1);
|
||||
expect(sessions[0]?.cwd).toBe(cwd);
|
||||
expect(sessions[0]?.path).toBe(path.join(root, "projects", encoded, `${id}.jsonl`));
|
||||
});
|
||||
});
|
||||
|
||||
describe("CodexSessionStore", () => {
|
||||
|
||||
Reference in New Issue
Block a user