/**
* 数据目录的路径单一真源 —— **项目内** `.mipham/` 与**用户级** `~/.mipham`。
*
* 目录名 `.mipham` 曾在 `src/` 里散落成几十处各自独立的字面量:用户级写
* `join(homedir(), '.mipham', …)`、项目内写 `join(cwd, '.mipham', …)`,两级的根
* 因此都没人守。现在字面量只剩 `shared/constants.ts` 的 `MIPHAM_DIR` 一处 ——
* 用户级一律经 `miphamHome()`,项目内一律经 `join(
, MIPHAM_DIR, …)`。
*
* 写入一律落在 `.mipham/`(我们自己的目录);`.claude/` 只保留**只读兼容** ——
* 早期版本把 worktree 建在 `.claude/worktrees/` 下,那些工作树今天仍要可列举、
* 可退出、可隔离。因此隔离判据必须同时认两个前缀:只认新前缀会让旧工作树
* 突然失去隔离保护(隔离度只许增不许减)。
*/
import { realpathSync } from 'node:fs'
import { homedir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { MIPHAM_DIR } from '../shared/constants.ts'
/** 只读兼容目录名。 */
export const LEGACY_CLAUDE_DIR = '.claude'
/**
* worktree 的识别标记(含结尾斜杠),新前缀在前。
* 用于从 `cwd` 反推 worktree 所属的项目根。
*/
export const WORKTREE_MARKERS = [
`${MIPHAM_DIR}/worktrees/`,
`${LEGACY_CLAUDE_DIR}/worktrees/`,
] as const
/** 新建 worktree 的根目录(绝对路径)。 */
export function worktreeRoot(cwd: string): string {
return join(cwd, MIPHAM_DIR, 'worktrees')
}
/** 历史与当前的全部 worktree 根目录,写入根在前。 */
export function worktreeRoots(cwd: string): string[] {
return [worktreeRoot(cwd), join(cwd, LEGACY_CLAUDE_DIR, 'worktrees')]
}
/**
* 规范化 worktree 路径,两侧都过一遍才谈得上比较。
*
* 叶子不存在是常态(工作树已被删、或路径是模型编出来的),此时 `realpathSync`
* 会抛 —— 那就只规范父目录、最后一段按原样留着,否则「叶子没了」会被误读成
* 「拼法不同」(明明同一个路径,却因为 P 的拼法与 git 打印的不同而判成不在)。
*/
function canonicalWorktreePath(path: string): string {
const trimmed = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path
try {
return realpathSync(trimmed)
} catch {
try {
return join(realpathSync(dirname(trimmed)), basename(trimmed))
} catch {
return trimmed
}
}
}
/**
* `git worktree list --porcelain` 里是否**确实**列出了 `target` 这个工作树。
*
* 不能用 `output.includes(target)`:那是子串判定,本机实测(真 git,建出 `w1`
* 与 `w10`)它错在三个方向 ——
* - `.../w1` 命中 **`.../w10` 那一行**(前缀当成同一个)⇒ 不存在被判成存在。
* EnterWorktree 那侧因此连 `w1` 都建不出来:明明没有,它报 already exists;
* - 带尾斜杠的 `.../w1/` 一行都不命中 ⇒ 存在被判成 not found;
* - git 打印 **realpath 拼法**(`mktemp -d /tmp/x` 建的在 porcelain 里是
* `/private/tmp/x/...`)⇒ 别名拼法一头都命中不了,而 EnterWorktree 的成功
* 文案里印的正是它自己算出来的那个拼法,模型照抄回来必然吃 not found。
*
* 判据是**相等**(名字比对),不是包含 —— 工作树列表里列的就是工作树根。
*/
export function listsWorktree(output: string, target: string): boolean {
const want = canonicalWorktreePath(target)
return output
.split('\n')
.filter((line) => line.startsWith('worktree '))
.some((line) => canonicalWorktreePath(line.slice('worktree '.length).trim()) === want)
}
/**
* 在 `cwd` 中定位 worktree 标记,返回项目根与命中的标记。
* 不在任何 worktree 内时返回 null。
*/
/**
* Locate the project root by looking for a worktree marker in `cwd`.
*
* The returned `root` has no trailing separator: callers compose it as
* `root + '/'` for a prefix compare, and a trailing slash there would make
* the pattern `${root}//` match nothing.
*/
export function findWorktreeMarker(cwd: string): { root: string; marker: string } | null {
for (const marker of WORKTREE_MARKERS) {
const index = cwd.indexOf(marker)
if (index !== -1) return { root: cwd.substring(0, index).replace(/\/+$/, ''), marker }
}
return null
}
/**
* 新建 workflow 脚本的目录(绝对路径)。与 worktree 同理:写入落在 `.mipham/`。
*
* 注意别与**运行产物**目录混淆:`~/.mipham/workflows//`(见
* `workflow/journal.ts`)同名但不同义,装的是 journal 与转录。两者靠「非递归
* readdir + 只收 .js」区分,属巧合而非设计,故本函数绝不返回那个根。
*/
export function workflowScriptDir(cwd: string): string {
return join(cwd, MIPHAM_DIR, 'workflows')
}
/**
* 全部可读的 workflow 脚本目录,写入根在前。
*
* 读侧必须同时认新旧前缀,否则升级后第一次 `/workflow save` 会失败 ——
* 上一次运行的 `.last-run.json` 还在旧目录里。用户级旧前缀也保留在列,
* 但**没有**对应的 `~/.mipham/workflows` 用户级脚本根:那里是运行产物的地盘。
*/
export function workflowScriptDirs(cwd: string): string[] {
return [
workflowScriptDir(cwd),
join(cwd, LEGACY_CLAUDE_DIR, 'workflows'),
join(homedir(), LEGACY_CLAUDE_DIR, 'workflows'),
]
}
/* ── 用户级数据根 ──────────────────────────────────────────────────────── */
/**
* 用户级数据根目录 `~/.mipham`,后可接任意子路径。
*
* `homedir()` 在**调用时**求值,与改造前的调用点语义一致 —— 调用点若在模块作用域,
* 本函数也在那时求值。这不是可有可无的细节:`config/loader.ts` 的 `MIPHAM_HOME` 正是
* 模块作用域捕获的,测试靠 `vi.mock('node:os')` + `vi.hoisted` 抢在 import 之前装
* mock 才拦得住它(见 `test/telemetry/index.test.ts` 顶部那条注释)。故本函数**不得**
* 改成在别处预先算好再传进来 —— 那会把「调用时求值」换成「导入时求值」。
*/
export function miphamHome(...segments: string[]): string {
return join(homedir(), MIPHAM_DIR, ...segments)
}