✨ feat(vscode): worktree 改建到 ~/Sources/worktree/<项目>/<slug>,接线 superpowers.worktreeDirectory 配置
实施流程不再硬编码 .claude/worktrees/<hash>: - worktree 目录名改用 spec/plan 的 slug(specs|plans 后那一级目录名),分支改为 feature/<slug> - 目录走 superpowers.worktreeDirectory 模板(package.json 新注册配置),支持 $project_root/$project_name/$feature_name 占位符及开头 ~ 展开,默认 ~/Sources/worktree/$project_name/$feature_name - worktree 现位于工作区外:创建前 mkdir -p 父目录,state/webview 改存绝对路径(下游消费方已兼容 isAbsolute) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -558,6 +558,48 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a human-readable slug from a spec/plan file path.
|
||||
*
|
||||
* spec/plan live as directories on disk
|
||||
* (docs/superpowers/{specs,plans}/<slug>/{spec,plan}.md), so the slug is the
|
||||
* path segment right after the `specs`/`plans` segment — that's the name the
|
||||
* user sees in the tree. Falls back to the parent dir name, then to a stable
|
||||
* 8-char sha256 of the path so the worktree dir name is never empty. Paths
|
||||
* come from gitea body comments and use `/` separators.
|
||||
*/
|
||||
function deriveSlug(planFile: string): string {
|
||||
const segments = planFile.split('/').filter(Boolean)
|
||||
const anchor = segments.findIndex(s => s === 'plans' || s === 'specs')
|
||||
if (anchor >= 0 && segments[anchor + 1]) {
|
||||
return segments[anchor + 1]
|
||||
}
|
||||
const parent = path.basename(path.dirname(planFile))
|
||||
if (parent && parent !== '.' && parent !== '/') {
|
||||
return parent
|
||||
}
|
||||
return createHash('sha256').update(planFile).digest('hex').slice(0, 8)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the `superpowers.worktreeDirectory` template into an ABSOLUTE path.
|
||||
* Placeholders: $project_root (workspace abs path), $project_name (its
|
||||
* basename), $feature_name (the slug). A leading `~` is expanded here because
|
||||
* git/execFile run without a shell and won't do tilde expansion themselves.
|
||||
* The result is resolved to absolute since the worktree now lives outside the
|
||||
* workspace and is stored/compared as an absolute path downstream.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off the end-to-end "implement this plan" flow:
|
||||
* 1. Detect repo + token, compute a stable feature_name from the plan path.
|
||||
@@ -639,10 +681,13 @@ export async function handleImplement(
|
||||
return
|
||||
}
|
||||
|
||||
const feature = createHash('sha256').update(planFile).digest('hex').slice(0, 8)
|
||||
const branch = `feature/${feature}`
|
||||
const relativeWorktreePath = `.claude/worktrees/${feature}`
|
||||
const worktreePath = path.join(workspaceRoot, relativeWorktreePath)
|
||||
// worktree 目录名用 spec/plan 的 slug(可读、与 spec 目录同名),不再用 planFile 的 hash。
|
||||
const slug = deriveSlug(planFile)
|
||||
const branch = `feature/${slug}`
|
||||
// 目录走 superpowers.worktreeDirectory 模板(设置里可改),默认 ~/Sources/worktree/<项目>/<slug>。
|
||||
const worktreeTemplate = workspace.getConfiguration('superpowers').get<string>('worktreeDirectory')
|
||||
|| '~/Sources/worktree/$project_name/$feature_name'
|
||||
const worktreePath = resolveWorktreeDir(worktreeTemplate, workspaceRoot, slug)
|
||||
|
||||
// Pre-flight: refuse if either the directory or the branch already
|
||||
// exists, since `git worktree add -b` would fail and we'd have to
|
||||
@@ -677,7 +722,7 @@ export async function handleImplement(
|
||||
}
|
||||
if (worktreeExists || branchExists) {
|
||||
void window.showErrorMessage(
|
||||
`feature ${feature} 的 worktree 或分支已存在,请先清理`,
|
||||
`feature ${slug} 的 worktree 或分支已存在,请先清理`,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -713,6 +758,9 @@ export async function handleImplement(
|
||||
// Create the worktree before anything else, so a worktree-add failure
|
||||
// doesn't leave half-written state behind.
|
||||
try {
|
||||
// worktree 现在建在工作区外(~/Sources/worktree/<项目>/...),父目录可能不存在;
|
||||
// git worktree add 不会创建多层父目录,先补齐。
|
||||
await fsp.mkdir(path.dirname(worktreePath), { recursive: true })
|
||||
await createWorktree({ workspaceRoot, worktreePath, branch })
|
||||
}
|
||||
catch (err) {
|
||||
@@ -759,7 +807,7 @@ export async function handleImplement(
|
||||
await panel.mergeIssueState(issueNumber, {
|
||||
column: 'in-progress',
|
||||
branch,
|
||||
worktreePath: relativeWorktreePath,
|
||||
worktreePath,
|
||||
implementStatus: 'running',
|
||||
})
|
||||
panel.postMessage({
|
||||
@@ -768,7 +816,7 @@ export async function handleImplement(
|
||||
patch: {
|
||||
column: 'in-progress',
|
||||
branch,
|
||||
worktreePath: relativeWorktreePath,
|
||||
worktreePath,
|
||||
implementStatus: 'running',
|
||||
worktreeExists: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user