✨ 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:
@@ -93,6 +93,16 @@
|
|||||||
"group": "navigation"
|
"group": "navigation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"title": "Superpowers",
|
||||||
|
"properties": {
|
||||||
|
"superpowers.worktreeDirectory": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "~/Sources/worktree/$project_name/$feature_name",
|
||||||
|
"markdownDescription": "运行 Plan 时创建 worktree 的目录模板。支持占位符 `$project_root`(工作区根目录的绝对路径)、`$project_name`(工作区目录名)、`$feature_name`(spec/plan 的 slug)。开头的 `~` 会展开为当前用户主目录。默认会把 worktree 建到 `~/Sources/worktree/<项目名>/<slug>`。"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -558,6 +558,48 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
|
|||||||
return true
|
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:
|
* Kick off the end-to-end "implement this plan" flow:
|
||||||
* 1. Detect repo + token, compute a stable feature_name from the plan path.
|
* 1. Detect repo + token, compute a stable feature_name from the plan path.
|
||||||
@@ -639,10 +681,13 @@ export async function handleImplement(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const feature = createHash('sha256').update(planFile).digest('hex').slice(0, 8)
|
// worktree 目录名用 spec/plan 的 slug(可读、与 spec 目录同名),不再用 planFile 的 hash。
|
||||||
const branch = `feature/${feature}`
|
const slug = deriveSlug(planFile)
|
||||||
const relativeWorktreePath = `.claude/worktrees/${feature}`
|
const branch = `feature/${slug}`
|
||||||
const worktreePath = path.join(workspaceRoot, relativeWorktreePath)
|
// 目录走 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
|
// Pre-flight: refuse if either the directory or the branch already
|
||||||
// exists, since `git worktree add -b` would fail and we'd have to
|
// exists, since `git worktree add -b` would fail and we'd have to
|
||||||
@@ -677,7 +722,7 @@ export async function handleImplement(
|
|||||||
}
|
}
|
||||||
if (worktreeExists || branchExists) {
|
if (worktreeExists || branchExists) {
|
||||||
void window.showErrorMessage(
|
void window.showErrorMessage(
|
||||||
`feature ${feature} 的 worktree 或分支已存在,请先清理`,
|
`feature ${slug} 的 worktree 或分支已存在,请先清理`,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -713,6 +758,9 @@ export async function handleImplement(
|
|||||||
// Create the worktree before anything else, so a worktree-add failure
|
// Create the worktree before anything else, so a worktree-add failure
|
||||||
// doesn't leave half-written state behind.
|
// doesn't leave half-written state behind.
|
||||||
try {
|
try {
|
||||||
|
// worktree 现在建在工作区外(~/Sources/worktree/<项目>/...),父目录可能不存在;
|
||||||
|
// git worktree add 不会创建多层父目录,先补齐。
|
||||||
|
await fsp.mkdir(path.dirname(worktreePath), { recursive: true })
|
||||||
await createWorktree({ workspaceRoot, worktreePath, branch })
|
await createWorktree({ workspaceRoot, worktreePath, branch })
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -759,7 +807,7 @@ export async function handleImplement(
|
|||||||
await panel.mergeIssueState(issueNumber, {
|
await panel.mergeIssueState(issueNumber, {
|
||||||
column: 'in-progress',
|
column: 'in-progress',
|
||||||
branch,
|
branch,
|
||||||
worktreePath: relativeWorktreePath,
|
worktreePath,
|
||||||
implementStatus: 'running',
|
implementStatus: 'running',
|
||||||
})
|
})
|
||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
@@ -768,7 +816,7 @@ export async function handleImplement(
|
|||||||
patch: {
|
patch: {
|
||||||
column: 'in-progress',
|
column: 'in-progress',
|
||||||
branch,
|
branch,
|
||||||
worktreePath: relativeWorktreePath,
|
worktreePath,
|
||||||
implementStatus: 'running',
|
implementStatus: 'running',
|
||||||
worktreeExists: true,
|
worktreeExists: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user