import { createHash } from 'node:crypto' import * as os from 'node:os' import * as path from 'node:path' export const DEFAULT_WORKTREE_TEMPLATE = '~/Sources/worktree/$project_name/$feature_name' /** * 设计文档 / 计划按 ECC 布局存放:`.claude/specs/.md`、`.claude/plans/.md`, * 文件名的 stem 就是 slug,两者共用同一个 stem。少数布局把文件名固定成 * `plan.md` / `spec.md`,这时 slug 退回父目录名;再兜底路径 hash,保证不为空。 */ export function deriveSlug(planFile: string): string { const stem = path.basename(planFile).replace(/\.md$/, '') if (stem && stem !== 'plan' && stem !== 'spec') return stem const parent = path.basename(path.dirname(planFile)) if (parent && parent !== '.' && parent !== '/') return parent return createHash('sha256').update(planFile).digest('hex').slice(0, 8) } /** 实施分支固定形如 `feature/`;接管方只有分支名,从这里反推 worktree 目录名。 */ export function slugFromBranch(branch: string): string { return branch.startsWith('feature/') ? branch.slice('feature/'.length) : branch } /** * 把 worktreeDirectory 模板展开成绝对路径。`~` 在这里展开:git/execFile 不经 shell, * 不会自己做 tilde 展开。 */ export function resolveWorktreeDir(template: string, workspaceRoot: string, slug: string): string { let p = template .split('$project_name') .join(path.basename(workspaceRoot)) .split('$project_root') .join(workspaceRoot) .split('$feature_name') .join(slug) if (p.startsWith('~')) p = path.join(os.homedir(), p.slice(1)) return path.resolve(p) }