docs(persona-chat): clarify auto-hook boundary #13
+43
-23
@@ -305,30 +305,57 @@ export function ensureClone(slug, area, url) {
|
||||
// 檔案搬運:工作副本 <-> clone
|
||||
// --------------------------------------------------------------------------- //
|
||||
|
||||
function listAreaFiles(root, area) {
|
||||
/**
|
||||
* 這一區在工作副本裡有哪些檔案。
|
||||
* 資料夾要**遞迴**收:`mindmap/threads/archive/`(睡眠時收起來的舊思維導圖)
|
||||
* 是子資料夾,只看第一層的話它整個不會被同步。
|
||||
*/
|
||||
export function listAreaFiles(root, area) {
|
||||
const out = [];
|
||||
const walk = (relDir) => {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = fs.readdirSync(path.join(root, relDir), { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const rel = path.posix.join(relDir, entry.name);
|
||||
if (entry.isDirectory()) walk(rel);
|
||||
else if (entry.isFile()) out.push(rel);
|
||||
}
|
||||
};
|
||||
for (const rel of AREAS[area].paths) {
|
||||
const abs = path.join(root, rel);
|
||||
if (rel.endsWith("/")) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = fs.readdirSync(abs, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile()) out.push(path.posix.join(rel.replace(/\/$/, ""), entry.name));
|
||||
}
|
||||
walk(rel.replace(/\/$/, ""));
|
||||
continue;
|
||||
}
|
||||
const abs = path.join(root, rel);
|
||||
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) out.push(rel);
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
// `-z`:路徑用 NUL 分隔、不做跳脫。少了它,含非 ASCII 的檔名(長期記憶的檔名就是中文的)
|
||||
// 會被 git 引號跳脫成 `"Memory-\350\267\250…"`,跟工作副本比對不上——結果是那些檔案
|
||||
// pull 時被靜靜略過、本機刪掉後也不會從遠端消失。
|
||||
function listTrackedFiles(dir) {
|
||||
const res = git(["ls-files"], dir);
|
||||
return res.ok ? res.stdout.split("\n").map((s) => s.trim()).filter(Boolean).sort() : [];
|
||||
const res = git(["ls-files", "-z"], dir);
|
||||
return res.ok ? res.stdout.split("\0").filter(Boolean).sort() : [];
|
||||
}
|
||||
|
||||
/** `git diff --name-only -z` → 檔名清單(理由同上)。 */
|
||||
function diffPaths(dir, args) {
|
||||
return git(["diff", "--name-only", "-z", ...args], dir).stdout.split("\0").filter(Boolean);
|
||||
}
|
||||
|
||||
/** `git status --porcelain -z` → 檔名清單(同樣為了非 ASCII 檔名而用 -z)。 */
|
||||
function statusPaths(dir) {
|
||||
return git(["status", "--porcelain", "-z"], dir)
|
||||
.stdout.split("\0")
|
||||
// git() 會把輸出整個 trim 掉,所以第一筆的狀態欄前導空白可能已經不見了(" M x" → "M x")
|
||||
.map((entry) => entry.replace(/^\s*[A-Z?!]{1,2}\s+/, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export const WIKI_MANIFEST = "_paths.json";
|
||||
@@ -623,19 +650,14 @@ export async function pullArea(slug, area, { code = null, owner = null, force =
|
||||
const dir = ensureClone(slug, area, repoUrl(host, theOwner, theCode, area));
|
||||
stageArea(slug, area, dir); // 先把本機現況放進 clone,才看得出本機動過什麼
|
||||
clearStaleIndexLock(dir);
|
||||
const localChanged = new Set(
|
||||
git(["status", "--porcelain"], dir)
|
||||
.stdout.split("\n")
|
||||
.map((l) => l.slice(3).trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const localChanged = new Set(statusPaths(dir));
|
||||
const fetched = git(["fetch", "--quiet", "origin"], dir);
|
||||
if (!fetched.ok) return { ok: false, area, reason: fetched.stderr || "fetch 失敗" };
|
||||
const head = git(["rev-parse", "--abbrev-ref", "HEAD"], dir).stdout || "main";
|
||||
const remoteRef = `origin/${head}`;
|
||||
const exists = git(["rev-parse", "--verify", "--quiet", remoteRef], dir);
|
||||
if (!exists.ok) return { ok: true, area, code: theCode, empty: true, changed: [] };
|
||||
const incoming = git(["diff", "--name-only", "HEAD", remoteRef], dir).stdout.split("\n").filter(Boolean);
|
||||
const incoming = diffPaths(dir, ["HEAD", remoteRef]);
|
||||
const conflicts = incoming.filter((f) => localChanged.has(f));
|
||||
if (conflicts.length && !force) {
|
||||
git(["checkout", "--", "."], dir);
|
||||
@@ -684,9 +706,7 @@ export async function verifyArea(slug, area, { code = null, owner = null } = {})
|
||||
pl.writeText(path.join(dir, "Icon.md"), wikiIconPage(slug, theCode));
|
||||
}
|
||||
const files = stageArea(slug, area, dir);
|
||||
const dirty = git(["status", "--porcelain"], dir)
|
||||
// git() 會把輸出 trim 掉,所以 porcelain 開頭那個空白可能已經不見了(" M x" → "M x")
|
||||
.stdout.split("\n").map((l) => l.replace(/^\s*[A-Z?!]{1,2}\s+/, "").trim()).filter(Boolean);
|
||||
const dirty = statusPaths(dir);
|
||||
git(["checkout", "--", "."], dir);
|
||||
git(["clean", "-qfd"], dir);
|
||||
return {
|
||||
|
||||
@@ -864,6 +864,14 @@ check("高頻資料在檔案區、低頻資料在 Wiki 區",
|
||||
covered("journal/2026-01.jsonl")[0] === "files" && covered("state/said.jsonl")[0] === "files" &&
|
||||
covered("IDENTITY.md")[0] === "wiki" && covered("memory/long-term/x.md")[0] === "wiki" &&
|
||||
covered("relations/graph.json")[0] === "wiki");
|
||||
// 分區的路徑有好幾個是資料夾,而資料夾裡還有資料夾:`mindmap/threads/archive/`
|
||||
// (睡眠時把太久沒動的思維導圖收進去)只看第一層的話整個不會被同步。
|
||||
check("同步範圍會遞迴進子資料夾(mindmap/threads/archive 以前整個漏掉)", (() => {
|
||||
const dir = path.join(pl.personaDir("alpha"), "mindmap", "threads", "archive");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "old-thread.mmd"), "%% 收起來的舊思維導圖\nflowchart TD\n");
|
||||
return gt.listAreaFiles(pl.personaDir("alpha"), "files").includes("mindmap/threads/archive/old-thread.mmd");
|
||||
})(), gt.listAreaFiles(pl.personaDir("alpha"), "files").join(", "));
|
||||
check("沒設定 Gitea 時同步只是略過,不會爆炸", (() => {
|
||||
const res = cli(["sync", "status", "--session", S_CODE]);
|
||||
const push = cli(["sync", "push", "--session", S_CODE], { expectOk: false });
|
||||
|
||||
Reference in New Issue
Block a user