✨ feat(vscode): 设置面板可配置 Claude profiles 目录并修正探测顺序
This commit is contained in:
1 parent
a3e557f9ee
commit
7c67d220db
13 files changed
+335
-35
No files matched your search
+129
-13
@@ -1,17 +1,25 @@
|
||||
/**
|
||||
* Lists Claude settings profiles from a hardcoded directory. Each `.json`
|
||||
* file there is treated as a profile whose `name` is its basename without
|
||||
* the extension and whose `path` is the absolute file path, suitable for
|
||||
* passing to `claude --settings <path>`.
|
||||
* Lists Claude settings profiles from a resolved directory.
|
||||
* Each `.json` file is a profile: `name` = basename without extension,
|
||||
* `path` = absolute path for `claude --settings <path>`.
|
||||
*
|
||||
* Failures (directory missing, unreadable, etc.) are swallowed — the
|
||||
* caller treats an empty list as "no profile selector".
|
||||
* 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'
|
||||
|
||||
const PROFILES_DIR = '/home/cruldra/Sources/cruldra-profile/claude-config/profiles'
|
||||
import { expandTilde } from '../git/worktree'
|
||||
|
||||
export interface ClaudeProfile {
|
||||
/** basename without .json */
|
||||
@@ -20,20 +28,100 @@ export interface ClaudeProfile {
|
||||
path: string
|
||||
}
|
||||
|
||||
/** Settings-panel override; empty/undefined means auto-detect. */
|
||||
let profilesDirOverride: string | undefined
|
||||
|
||||
/**
|
||||
* List Claude settings profiles from the hardcoded directory. Returns an
|
||||
* empty array if the directory is missing or unreadable; does not throw.
|
||||
* Sorted alphabetically by name.
|
||||
* 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 ? 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<ClaudeProfile[]> {
|
||||
const dir = getProfilesDir()
|
||||
try {
|
||||
const entries = await readdir(PROFILES_DIR)
|
||||
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(PROFILES_DIR, entry) })
|
||||
profiles.push({ name, path: path.join(dir, entry) })
|
||||
}
|
||||
profiles.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return profiles
|
||||
@@ -42,3 +130,31 @@ export async function listClaudeProfiles(): Promise<ClaudeProfile[]> {
|
||||
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')
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile for PR diff summaries / background content generation.
|
||||
* Prefer `deepseek-v4-pro`, then `deepseek.json`.
|
||||
*/
|
||||
export function getPrDiffSummaryProfilePath(): string {
|
||||
const dir = getProfilesDir()
|
||||
for (const name of ['deepseek-v4-pro.json', 'deepseek.json']) {
|
||||
const p = path.join(dir, name)
|
||||
if (isReadableFile(p))
|
||||
return p
|
||||
}
|
||||
return path.join(dir, 'deepseek-v4-pro.json')
|
||||
}
|
||||
Reference in new issue
Block a user