非纯格式的改动: - logger:info 级日志原先走 console.log(no-console 禁止),改为通过 VS Code LogOutputChannel(Output 面板 "Superpowers")输出 info/warn/error,并在 activate 时注册 dispose - esbuild.mjs:顶层 await 收进 async main(),失败时退出码 1;构建脚本打印 进度属正当输出,文件级关闭 no-console 并注明原因 - KanbanPanel 资源路径正则:去掉 `\.\/[^"]+` 分支(已被 `[^"/][^"]*` 完全 覆盖),匹配结果不变 - webhook/server:onError/onListening 互相引用,改为函数声明消除先用后定义 - SettingsModal:删掉针对未启用规则 react-hooks/exhaustive-deps 的失效 disable 注释,effect 依赖 [open] 保持不变(按打开时机重置表单是有意的) - issues.ts 提示词里两处全角空格换成普通空格 - 其余为 node:process / node:buffer 显式导入与 switch 拆行 Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
148 lines
4.1 KiB
TypeScript
148 lines
4.1 KiB
TypeScript
/**
|
||
* 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>`.
|
||
*
|
||
* 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<ClaudeProfile[]> {
|
||
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')
|
||
}
|