Files
superwork/vscode/src/git/worktreeHooks.ts
T
cruldra dae1232f19 🎨 style(vscode): 清零 lint 欠账
非纯格式的改动:
- logger:info 级日志原先走 console.log(no-console 禁止),改为通过 VS Code
  LogOutputChannel(Output 面板 "Superpowers")输出 info/warn/error,并在
  activate 时注册 dispose
- esbuild.mjs:顶层 await 收进 async main(),失败时退出码 1;构建脚本打印
  进度属正当输出,文件级关闭 no-console 并注明原因
- KanbanPanel 资源路径正则:去掉 `\.\/[^"]+` 分支(已被 `[^"/][^"]*` 完全
  覆盖),匹配结果不变
- webhook/server:onError/onListening 互相引用,改为函数声明消除先用后定义
- SettingsModal:删掉针对未启用规则 react-hooks/exhaustive-deps 的失效
  disable 注释,effect 依赖 [open] 保持不变(按打开时机重置表单是有意的)
- issues.ts 提示词里两处全角空格换成普通空格
- 其余为 node:process / node:buffer 显式导入与 switch 拆行

Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
2026-08-31 22:33:11 +08:00

145 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Spawn user-provided shell scripts around the worktree create / remove
* lifecycle. Lets the user perform setup (copy `.env`, open IDE, …) and
* teardown (close IDE, archive logs, …) without modifying extension code.
*
* Behavior contract:
* - script absent on disk → silently return `skipped` (default state).
* - script exits 0 → `ok`.
* - non-zero exit / spawn ENOENT / timeout → returned as a non-`ok`
* status; the caller surfaces a `warn`-style toast but never aborts
* the surrounding flow.
*
* Env vars injected into the child process:
* - WORKTREE_PATH — absolute path of the worktree
* - WORKSPACE_ROOT — absolute path of the main workspace
* - BRANCH — feature branch name
* - ISSUE_NUMBER — gitea issue number (decimal string)
* - MAIN_BRANCH — settings.devBranch (default 'main')
*/
import { execFile } from 'node:child_process'
import { existsSync } from 'node:fs'
import * as path from 'node:path'
import process from 'node:process'
const DEFAULT_POST_CREATE_REL = '.spx/worktree-post-create.sh'
const DEFAULT_PRE_REMOVE_REL = '.spx/worktree-pre-remove.sh'
const DEFAULT_IMPL_TAB_PRE_CREATE_REL = '.spx/impl-tab-pre-create.sh'
const DEFAULT_IMPL_TAB_POST_CLOSE_REL = '.spx/impl-tab-post-close.sh'
const HOOK_TIMEOUT_MS = 30_000
export interface HookContext {
workspaceRoot: string
worktreePath: string
branch: string
issueNumber: number
mainBranch: string
/**
* User-configured override path. Empty / undefined → use the default
* `<workspaceRoot>/.spx/worktree-*.sh` location.
*/
customScriptPath?: string
}
export type HookStatus = 'skipped' | 'ok' | 'failed' | 'timeout' | 'enoent'
export interface HookResult {
status: HookStatus
exitCode?: number
stdout?: string
stderr?: string
errorMessage?: string
scriptPath?: string
}
function resolveScriptPath(ctx: HookContext, defaultRelPath: string): string {
const custom = (ctx.customScriptPath ?? '').trim()
if (custom.length > 0)
return path.isAbsolute(custom) ? custom : path.join(ctx.workspaceRoot, custom)
return path.join(ctx.workspaceRoot, defaultRelPath)
}
async function runHook(ctx: HookContext, defaultRelPath: string): Promise<HookResult> {
const scriptPath = resolveScriptPath(ctx, defaultRelPath)
if (!existsSync(scriptPath))
return { status: 'skipped', scriptPath }
return new Promise<HookResult>((resolve) => {
execFile(
'bash',
[scriptPath],
{
cwd: ctx.worktreePath,
env: {
...process.env,
WORKTREE_PATH: ctx.worktreePath,
WORKSPACE_ROOT: ctx.workspaceRoot,
BRANCH: ctx.branch,
ISSUE_NUMBER: String(ctx.issueNumber),
MAIN_BRANCH: ctx.mainBranch,
},
timeout: HOOK_TIMEOUT_MS,
},
(err, stdout, stderr) => {
if (!err) {
resolve({ status: 'ok', exitCode: 0, stdout, stderr, scriptPath })
return
}
const errCode = (err as NodeJS.ErrnoException).code
// ENOENT here means `bash` itself isn't on PATH (script presence
// was already checked above). Surface it distinctly so the warn
// toast can be informative.
if (errCode === 'ENOENT') {
resolve({
status: 'enoent',
errorMessage: 'bash 不存在(PATH 中找不到)',
stdout,
stderr,
scriptPath,
})
return
}
// Node's execFile reports timeout via `ETIMEDOUT` on err.code (and
// also kills the process with SIGTERM by default).
if (errCode === 'ETIMEDOUT' || (err as NodeJS.ErrnoException & { killed?: boolean }).killed) {
resolve({
status: 'timeout',
errorMessage: `脚本超时(${HOOK_TIMEOUT_MS / 1000}s)`,
stdout,
stderr,
scriptPath,
})
return
}
resolve({
status: 'failed',
exitCode: typeof (err as NodeJS.ErrnoException & { code?: number | string }).code === 'number'
? (err as unknown as { code: number }).code
: undefined,
errorMessage: err.message,
stdout,
stderr,
scriptPath,
})
},
)
})
}
export function runPostCreateHook(ctx: HookContext): Promise<HookResult> {
return runHook(ctx, DEFAULT_POST_CREATE_REL)
}
export function runPreRemoveHook(ctx: HookContext): Promise<HookResult> {
return runHook(ctx, DEFAULT_PRE_REMOVE_REL)
}
export function runImplTabPreCreateHook(ctx: HookContext): Promise<HookResult> {
return runHook(ctx, DEFAULT_IMPL_TAB_PRE_CREATE_REL)
}
export function runImplTabPostCloseHook(ctx: HookContext): Promise<HookResult> {
return runHook(ctx, DEFAULT_IMPL_TAB_POST_CLOSE_REL)
}