/** * Lists Claude settings profiles from a resolved directory. * Each `.json` file is a profile: `name` = basename without extension, * `path` = absolute path for `claude --settings `. * * Resolution order for the profiles directory: * 1. `$SUPERPOWERS_PROFILES_DIR` (env override) * 2. settings `profilesDirectory` via setProfilesDirOverride (supports ~) * 3. auto candidates (first readable): * ~/Sources/cruldra-profile/claude-config/profiles * ~/.claude/profile-settings * ~/.claude/profiles * 4. fallback to the cruldra-profile path for error messages * * Missing/unreadable dirs yield an empty list — never throws. */ import { accessSync, constants } from 'node:fs' import { readdir } from 'node:fs/promises' import { homedir } from 'node:os' import * as path from 'node:path' import process from 'node:process' import { expandTilde } from '../git/worktree' export interface ClaudeProfile { /** basename without .json */ name: string /** absolute path */ path: string } /** Settings-panel override; empty/undefined means auto-detect. */ let profilesDirOverride: string | undefined /** * Apply settings `profilesDirectory` (or clear with empty/undefined). * Call on activate, settings open, and after settings save. */ export function setProfilesDirOverride(dir: string | undefined): void { const t = dir?.trim() profilesDirOverride = t || undefined } function isReadableDir(dir: string): boolean { try { accessSync(dir, constants.R_OK) return true } catch { return false } } function isReadableFile(file: string): boolean { try { accessSync(file, constants.R_OK) return true } catch { return false } } /** * Resolve the Claude profiles directory without hardcoding a user home path. * Prefers the first existing candidate; falls back to the conventional path * under `$HOME` so error messages stay meaningful when nothing exists yet. */ export function getProfilesDir(): string { // ① env 显式覆盖优先 const envOverride = process.env.SUPERPOWERS_PROFILES_DIR?.trim() if (envOverride) return path.resolve(envOverride) // ② 设置面板 override(支持 ~) if (profilesDirOverride) return path.resolve(expandTilde(profilesDirOverride)) // ③ auto:真实 profile 源优先,再兼容常见链接/目录 const home = homedir() const candidates = [ path.join(home, 'Sources', 'cruldra-profile', 'claude-config', 'profiles'), path.join(home, '.claude', 'profile-settings'), path.join(home, '.claude', 'profiles'), ] for (const dir of candidates) { if (isReadableDir(dir)) return dir } return candidates[0]! } /** * Remap a stored profile path (often an absolute path from another machine) * onto the current profiles directory by basename when the original is missing. */ export function resolveProfilePath(stored: string | undefined): string { const raw = stored?.trim() if (!raw) return getDefaultProfilePath() if (isReadableFile(raw)) return raw const remapped = path.join(getProfilesDir(), path.basename(raw)) if (isReadableFile(remapped)) return remapped return raw } /** * List Claude settings profiles. Empty array if the directory is missing * or unreadable. Sorted alphabetically by name. */ export async function listClaudeProfiles(): Promise { const dir = getProfilesDir() try { const entries = await readdir(dir) const profiles: ClaudeProfile[] = [] for (const entry of entries) { if (!entry.endsWith('.json')) continue const name = entry.slice(0, -'.json'.length) profiles.push({ name, path: path.join(dir, entry) }) } profiles.sort((a, b) => a.name.localeCompare(b.name)) return profiles } catch { return [] } } /** * Default profile for new sessions: prefer `offical.json` (legacy spelling), * then `official.json`. Path is always under getProfilesDir(). */ export function getDefaultProfilePath(): string { const dir = getProfilesDir() for (const name of ['offical.json', 'official.json']) { const p = path.join(dir, name) if (isReadableFile(p)) return p } return path.join(dir, 'offical.json') }