feat(vscode): 提交代码按钮可配置 Claude profile

This commit is contained in:
2026-07-25 07:29:05 +08:00
parent e9543d37ce
commit 84a5cda8d2
11 changed files with 269 additions and 9 deletions
+113
View File
@@ -0,0 +1,113 @@
import { accessSync, constants, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
DEFAULT_COMMIT_PROFILE_NAME,
resolveCommitProfileForRun,
seedCommitProfilePath,
} from './commitProfile'
const tempDirs: string[] = []
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop()
if (dir)
rmSync(dir, { recursive: true, force: true })
}
})
function tempDir(): string {
const dir = mkdtempSync(path.join(os.tmpdir(), 'commit-profile-'))
tempDirs.push(dir)
return dir
}
describe('seedCommitProfilePath', () => {
it('key 缺失时预填 deepseek-v4-pro 的 path', () => {
const profiles = [
{ name: 'grok-4.5', path: '/p/grok-4.5.json' },
{ name: DEFAULT_COMMIT_PROFILE_NAME, path: '/p/deepseek-v4-pro.json' },
]
expect(seedCommitProfilePath(undefined, profiles)).toBe('/p/deepseek-v4-pro.json')
})
it('key 缺失且无 deepseek 时返回空串', () => {
expect(seedCommitProfilePath(undefined, [{ name: 'grok-4.5', path: '/p/g.json' }])).toBe('')
})
it('key 已存在为空串时尊重用户清空', () => {
expect(seedCommitProfilePath('', [{ name: DEFAULT_COMMIT_PROFILE_NAME, path: '/p/d.json' }])).toBe('')
})
it('key 已存在为 path 时原样返回', () => {
expect(seedCommitProfilePath('/custom/x.json', [{ name: DEFAULT_COMMIT_PROFILE_NAME, path: '/p/d.json' }])).toBe('/custom/x.json')
})
})
describe('resolveCommitProfileForRun', () => {
it('空串 → empty', () => {
expect(resolveCommitProfileForRun(' ', p => p, () => true)).toEqual({ ok: false, reason: 'empty' })
})
it('文件不可读 → unreadable', () => {
const dir = tempDir()
const missing = path.join(dir, 'gone.json')
const result = resolveCommitProfileForRun(
missing,
p => p,
p => {
try {
accessSync(p, constants.R_OK)
return true
}
catch {
return false
}
},
)
expect(result).toEqual({ ok: false, reason: 'unreadable', profilePath: missing })
})
it('可读 path → ok', () => {
const dir = tempDir()
const file = path.join(dir, 'deepseek-v4-pro.json')
writeFileSync(file, '{}')
const result = resolveCommitProfileForRun(
file,
p => p,
p => {
try {
accessSync(p, constants.R_OK)
return true
}
catch {
return false
}
},
)
expect(result).toEqual({ ok: true, profilePath: file })
})
it('经 resolvePath remap 后再检查可读', () => {
const dir = tempDir()
const local = path.join(dir, 'deepseek-v4-pro.json')
writeFileSync(local, '{}')
const foreign = '/Users/other/.claude/profiles/deepseek-v4-pro.json'
const result = resolveCommitProfileForRun(
foreign,
() => local,
p => {
try {
accessSync(p, constants.R_OK)
return true
}
catch {
return false
}
},
)
expect(result).toEqual({ ok: true, profilePath: local })
})
})
+49
View File
@@ -0,0 +1,49 @@
/**
* 工具栏「提交代码」所用 profile 的解析与默认 seed。
* 纯逻辑:不碰 VS Code API,便于单测。
*/
export const DEFAULT_COMMIT_PROFILE_NAME = 'deepseek-v4-pro'
export interface ProfileRef {
name: string
path: string
}
/**
* 升级迁移:stored 里从未出现 `commitProfilePath` key 时,
* 若本机 profiles 有 deepseek-v4-pro 则预填其 path,否则 ''。
* 已出现 key(含用户主动清空的 '')一律原样返回。
*/
export function seedCommitProfilePath(
stored: string | undefined,
profiles: ProfileRef[],
): string {
if (typeof stored === 'string')
return stored
const hit = profiles.find(p => p.name === DEFAULT_COMMIT_PROFILE_NAME)
return hit?.path ?? ''
}
export type CommitProfileResolution
= | { ok: true, profilePath: string }
| { ok: false, reason: 'empty' }
| { ok: false, reason: 'unreadable', profilePath: string }
/**
* 提交前解析:trim → 空失败 → resolvePath → 可读性检查。
* resolvePath / isReadable 由调用方注入(跨机 remap / fs)。
*/
export function resolveCommitProfileForRun(
raw: string,
resolvePath: (stored: string) => string,
isReadable: (profilePath: string) => boolean,
): CommitProfileResolution {
const trimmed = raw.trim()
if (!trimmed)
return { ok: false, reason: 'empty' }
const profilePath = resolvePath(trimmed)
if (!isReadable(profilePath))
return { ok: false, reason: 'unreadable', profilePath }
return { ok: true, profilePath }
}