Files
JianMiauandClaude Fable 5 e591475d4c 加上免安裝版,寵物來源改成執行目錄的 pets
摘要:
新增 portable 單檔與 zip 資料夾兩種免安裝產物;寵物一律從執行目錄的 pets/ 掃,設定檔不再累積程式自己算出來的路徑。

根本原因:
1. 只有 NSIS 安裝檔,沒有點兩下就能跑的版本。
2. 內附寵物被 asarUnpack 埋在 resources/app.asar.unpacked/pets,使用者要新增寵物得先鑽進 resources 資料夾。
3. loadConfig 會把算出來的來源路徑寫回 config.json,程式一搬家或在安裝版與 portable 之間切換,petSources 就越積越多失效的絕對路徑(實際已累積出 dist/win-unpacked/resources/app.asar.unpacked/pets 這種殘留)。
4. CODEX_PETS_DIR 用到 HOME,但 const HOME 宣告在它下面,會在啟動時就 TDZ ReferenceError。

影響:
沒有免安裝選項;新增寵物的路徑不直覺;設定檔會長出一堆死路徑;且上一版打包後的程式其實一啟動就會崩潰。

修法:
- win target 加上 portable 與 zip;portable 設 unpackDirName=ClaudePet、requestExecutionLevel=user。
- pets/ 改用 extraFiles 放到 exe 旁邊(不再進 asar),APP_ROOT 打包後取 process.execPath 的目錄。
- portable 的 NSIS 外殼每次啟動都重解壓、結束就刪整個目錄,因此:hook 腳本另外複製到 ~/.claude-pet/hook/ 並註冊該穩定路徑(否則寵物沒開時 Claude Code 每次觸發 hook 都找不到檔案);使用者寵物放那顆 exe 旁邊;開機自動啟動改用 PORTABLE_EXECUTABLE_FILE。
- petSources 語意改為「只存使用者自己加的額外路徑」;執行目錄的 pets 與 ~/.codex/pets 為隱含來源,由 petSearchDirs() 組出實際掃描清單,載入時剔除隱含項與不存在的路徑。同 id 以使用者自己放的優先。
- 把 HOME/CONFIG_DIR 移到常數區最前面,修掉 TDZ。
- 內附第二隻寵物 xiao-nian-realistic。

驗證:
以 ELECTRON_RUN_AS_NODE 執行打包後的 exe(不開視窗):
- 一般模式與模擬 portable 模式下的 pets 路徑、hook 路徑、autostart 指令皆正確。
- 用假的 USERPROFILE 實跑 install/uninstall:hook 腳本確實複製、指令含空白路徑會加引號、其他人的 Stop/PreToolUse hook 與 model 設定在安裝與移除後都完整保留、備份有產生。
- 帶殘留路徑的設定檔會被正確清理,掃描順序為「使用者 pets → 內附 pets → ~/.codex/pets → 額外路徑」。
- 從 app.asar 讀回 main.js 確認出貨的程式碼順序正確。
- 以純 node 載入 main.js,確認常數區與 loadConfig 不再有 TDZ。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:11:20 +08:00

55 lines
2.0 KiB
JavaScript
Raw Permalink 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.
"use strict";
// Windows 開機自動啟動:HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ClaudePet
const path = require("node:path");
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const { isPackaged, portableExe } = require("./paths");
const APP_DIR = path.resolve(__dirname, "..");
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
const VALUE_NAME = "ClaudePet";
function electronExe() {
return path.join(APP_DIR, "node_modules", "electron", "dist", "electron.exe");
}
function command() {
// 打包後就是那顆 exe 本身。portable 版的 process.execPath 會指到 %TEMP% 的解壓副本,
// 要用 PORTABLE_EXECUTABLE_FILE 才是使用者手上那顆 exe。
if (isPackaged()) return `"${portableExe() || process.execPath}"`;
return `"${electronExe()}" "${APP_DIR}"`;
}
function reg(args) {
const r = spawnSync("reg", args, { encoding: "utf8", windowsHide: true });
return { ok: r.status === 0, stdout: r.stdout || "", stderr: r.stderr || "" };
}
function isInstalled() {
if (process.platform !== "win32") return false;
return reg(["query", RUN_KEY, "/v", VALUE_NAME]).ok;
}
function currentValue() {
const r = reg(["query", RUN_KEY, "/v", VALUE_NAME]);
if (!r.ok) return null;
const m = r.stdout.match(/REG_SZ\s+(.+)$/m);
return m ? m[1].trim() : null;
}
function install() {
if (process.platform !== "win32") throw new Error("只支援 Windows");
if (!isPackaged() && !fs.existsSync(electronExe())) throw new Error(`找不到 Electron${electronExe()}(先執行 npm install`);
const r = reg(["add", RUN_KEY, "/v", VALUE_NAME, "/t", "REG_SZ", "/d", command(), "/f"]);
if (!r.ok) throw new Error(r.stderr || r.stdout || "reg add 失敗");
return command();
}
function uninstall() {
if (process.platform !== "win32") return false;
const r = reg(["delete", RUN_KEY, "/v", VALUE_NAME, "/f"]);
return r.ok;
}
module.exports = { install, uninstall, isInstalled, currentValue, command, electronExe, VALUE_NAME, RUN_KEY };