migrations/ — 配置迁移
目录: src/migrations/
Claude Code 演进很快——数据格式也在变。migrations/ 确保老版本数据能升级到新版本。
为什么需要迁移?
v1.0: ~/.claude/config.json { "apiKey": "..." }
v1.5: ~/.claude/config.json { "credentials": { "anthropic": "..." } }
v2.0: ~/.claude/credentials.json (分离出来)
用户升级 Claude Code 不能手动迁移数据——程序自动处理。
迁移架构
interface Migration {
from: string // "1.5.0"
to: string // "2.0.0"
apply(data: any): Promise<any>
}
const MIGRATIONS: Migration[] = [
migration_1_0_to_1_5,
migration_1_5_to_2_0,
migration_2_0_to_2_1,
]
迁移流程
示例迁移
config 1.0 → 1.5
const migration_1_0_to_1_5: Migration = {
from: '1.0.0',
to: '1.5.0',
async apply(data) {
return {
...data,
credentials: {
anthropic: data.apiKey // 重命名
},
apiKey: undefined, // 删除老字段
version: '1.5.0'
}
}
}
credentials 分离
const migration_1_5_to_2_0: Migration = {
from: '1.5.0',
to: '2.0.0',
async apply(data) {
// 把 credentials 挪到独立文件
if (data.credentials) {
await writeFile(
'~/.claude/credentials.json',
JSON.stringify(data.credentials, null, 2),
{ mode: 0o600 }
)
}
delete data.credentials
return { ...data, version: '2.0.0' }
}
}
备份策略
迁移前总是备份:
async function backupBeforeMigration(path: string) {
const backup = `${path}.backup-${Date.now()}`
await fs.copyFile(path, backup)
// 保留最近 5 个备份
await cleanupOldBackups(path, 5)
return backup
}
回滚
迁移失败自动回滚:
async function runMigrations(data: any, from: string, to: string) {
const backup = await backupBeforeMigration(configPath)
try {
const chain = findChain(from, to)
for (const migration of chain) {
data = await migration.apply(data)
}
return data
} catch (e) {
// 还原
await fs.copyFile(backup, configPath)
throw new MigrationError(`Failed, restored backup`, e)
}
}
跨版本链
// 用户从 1.0 升到 2.0
findChain('1.0.0', '2.0.0')
// => [migration_1_0_to_1_5, migration_1_5_to_2_0]
function findChain(from: string, to: string): Migration[] {
const chain: Migration[] = []
let current = from
while (current !== to) {
const next = MIGRATIONS.find(m => m.from === current)
if (!next) throw new Error(`No migration path from ${current}`)
chain.push(next)
current = next.to
}
return chain
}
幂等性
async function migrateIfNeeded(path: string) {
const data = JSON.parse(await fs.readFile(path, 'utf8'))
if (data.version === CURRENT_VERSION) {
return data // 已经是最新,跳过
}
return runMigrations(data, data.version ?? '1.0.0', CURRENT_VERSION)
}
可反复执行——无版本差异时 noop。
迁移文件
除了 config,还有其他文件要迁移:
credentials.json
config.json
permissions.json
memory/*.md ← 有时 frontmatter 字段要改
sessions/*.jsonl ← 有时消息格式要改
tasks/tasks.json
plugins/*/plugin.json
每种有独立迁移链。
测试
test('migrates 1.0 config to 2.0', async () => {
const old = { apiKey: 'sk-...', version: '1.0.0' }
const migrated = await migrate(old, '1.0.0', '2.0.0')
expect(migrated.version).toBe('2.0.0')
expect(migrated.apiKey).toBeUndefined()
// credentials 被分离到单独文件
const creds = JSON.parse(await readFile('credentials.json'))
expect(creds.anthropic).toBe('sk-...')
})
迁移日志
async function migrate(data, from, to) {
log(`Migrating config from ${from} to ${to}`)
const chain = findChain(from, to)
for (const m of chain) {
log(` Applying ${m.from} → ${m.to}`)
data = await m.apply(data)
}
log(`Migration complete`)
return data
}
用户手动迁移
claude migrate --dry-run
# 打印要做什么但不执行
claude migrate --backup-only
# 只备份
claude migrate --rollback <backup-id>
# 回滚
Breaking Change 处理
某些迁移不可自动:
const migration_2_0_to_3_0: Migration = {
from: '2.0.0',
to: '3.0.0',
async apply(data) {
if (data.plugins && data.plugins.length > 0) {
throw new ManualMigrationRequired(
'Plugin API changed in 3.0. Please reinstall all plugins.'
)
}
return { ...data, version: '3.0.0' }
}
}
告诉用户要做什么——不自动破坏数据。
值得学习的点
- 自动迁移 — 用户无感升级
- 备份优先 — 任何迁移都先备份
- 自动回滚 — 失败安全
- 跨版本链 — 1.0 → 2.0 经过 1.5
- 幂等 — 重复运行无副作用
- 有日志 — 可 debug
- Manual migration 信号 — 不默默破坏