Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/core/config-health-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,35 @@ describe('readSnippet', () => {
expect(snippet.length).toBe(50);
});
});

describe('resolveWorkspaceRoot', () => {
it('uses a Claude workspace path that is already the project root', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-claude-root-'));
try {
const resolved = resolveWorkspaceRoot('claude--home-me-proj', { id: 'claude--home-me-proj', name: 'proj', path: root });
expect(resolved).toBe(root);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

it('still reads cwd from a session file when the path is a .claude/projects store', () => {
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-claude-store-'));
const projectRoot = path.join(base, 'real-project');
const store = path.join(base, '.claude', 'projects', '-tmp-real-project');
fs.mkdirSync(projectRoot, { recursive: true });
fs.mkdirSync(store, { recursive: true });
fs.writeFileSync(path.join(store, 's.jsonl'), JSON.stringify({ cwd: projectRoot }) + '\n');
try {
expect(resolveWorkspaceRoot('claude-x', { id: 'claude-x', name: 'x', path: store })).toBe(projectRoot);
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});

it('returns null for a Claude workspace whose path no longer exists', () => {
const gone = path.join(os.tmpdir(), 'cfg-claude-missing-' + Date.now());
expect(resolveWorkspaceRoot('claude-gone', { id: 'claude-gone', name: 'gone', path: gone })).toBeNull();
});
});

17 changes: 16 additions & 1 deletion src/core/config-health-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,29 @@ function firstStringProperty(value: Record<string, unknown>, ...keys: string[]):

export function resolveWorkspaceRoot(id: string, ws: Workspace): string | null {
if (id.startsWith('claude-')) {
// `ws.path` is normally the project root already, as it is for the other
// CLI harnesses. Fall back to reading `cwd` out of a session file for
// workspaces whose path still points at ~/.claude/projects/<encoded>.
if (isProjectRoot(ws.path)) return ws.path;
return resolveClaudeRoot(ws.path);
}
if (id.startsWith('codex-') || id.startsWith('opencode-')) {
return fs.existsSync(ws.path) ? ws.path : null;
return isProjectRoot(ws.path) ? ws.path : null;
}
return resolveVsCodeRoot(ws.path) ?? resolveCLIRoot(ws.path);
}

/** True when the path is an existing directory that is not a Claude session
* store (those hold .jsonl transcripts, not the user's project files). */
function isProjectRoot(p: string): boolean {
try {
if (!fs.statSync(p).isDirectory()) return false;
} catch {
return false;
}
return path.basename(path.dirname(p)) !== 'projects' || !path.dirname(p).includes('.claude');
}

function resolveVsCodeRoot(storagePath: string): string | null {
const wsJson = path.join(storagePath, 'workspace.json');
const data = readJsonFile(wsJson);
Expand Down