Files
claude-pet/lib/hooks-installer.js
T
JianMiauandClaude Fable 5 56fd8448ba 用 electron-builder 打包成 Windows exe
摘要:
加上 electron-builder 設定,`npm run dist` 產出 NSIS 安裝檔與免安裝版,並修正打包後才會出現的路徑問題。

根本原因:
原本三處路徑都假設程式是攤在資料夾裡跑的,打包成 asar 後會壞:
1. hook 腳本路徑會落在 app.asar 內,而 Claude Code 是用 `node <路徑>` 執行它,node 讀不到 asar 裡的檔案,hook 會直接失敗。
2. pets 資料夾同樣在 asar 內,使用者無法自己丟寵物進去,選單「開啟寵物資料夾」也開不起來。
3. 開機自動啟動寫入的是 `electron.exe + 專案路徑`,打包後沒有 node_modules,登錄值會指向不存在的檔案。

影響:
沒有可散佈的執行檔;直接打包的話 hook、換寵物、開機啟動三個功能都會壞掉。

修法:
- 新增 lib/paths.js:unpacked() 把 app.asar 換成 app.asar.unpacked,isPackaged() 以 process.defaultApp 判斷是否為打包版。
- build.asarUnpack 把 hook/ 與 pets/ 解到 asar 外,hooks-installer 與 main.js 的 PETS_DIR 都改走 unpacked()。
- autostart 打包後改用 process.execPath,並跳過開發版才需要的 electron.exe 存在檢查。
- NSIS 設為 per-user、可改安裝路徑、建立桌面與開始選單捷徑,解除安裝不刪 ~/.claude-pet。
- 新增 build/icon.ico(16–256,七種尺寸),取自小念 spritesheet 第 0 列第 6 欄的頭肩方形裁切。
- dist/ 加入 .gitignore;README 補上「打包成 exe」一節與安裝檔的使用方式。

驗證:
以 ELECTRON_RUN_AS_NODE 執行打包後的 exe(不開視窗)載入打包內的 lib,確認 isPackaged 為 true、hook 路徑指到 app.asar.unpacked 且檔案存在、autostart 指到 exe 本身、pets 掃得到 xiao-nian、renderer 讀得到 asar 內的 index.html;另確認 exe 版本資訊與七種尺寸圖示皆已嵌入。

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

114 lines
3.6 KiB
JavaScript
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.
"use strict";
// 把 Claude Pet 的 hook 合併進 ~/.claude/settings.json(保留使用者原本的設定與其他 hooks)
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { unpacked } = require("./paths");
const APP_DIR = path.resolve(__dirname, "..");
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
const BACKUP_DIR = path.join(CLAUDE_DIR, "backups");
// node 執行不了 asar 內的檔案,打包後要指到解開的那份
const HOOK_SCRIPT = unpacked(path.join(APP_DIR, "hook", "claude-pet-hook.js"));
const MARKER = "claude-pet-hook";
// Claude Code 2.1.x 支援的 hook 事件(對應到寵物狀態)
const EVENTS = [
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"PermissionRequest",
"Notification",
"Stop",
"StopFailure",
"SubagentStart",
"SubagentStop",
"PreCompact",
];
function hookCommand() {
const p = HOOK_SCRIPT.replace(/\\/g, "/");
return /\s/.test(p) ? `node "${p}"` : `node ${p}`;
}
function readSettings() {
try {
const text = fs.readFileSync(SETTINGS_PATH, "utf8");
return text.trim() ? JSON.parse(text) : {};
} catch (err) {
if (err.code === "ENOENT") return {};
throw new Error(`無法解析 ${SETTINGS_PATH}${err.message}`);
}
}
function writeSettings(settings) {
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
const tmp = `${SETTINGS_PATH}.${process.pid}.tmp`;
fs.writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
fs.renameSync(tmp, SETTINGS_PATH);
}
function backup() {
if (!fs.existsSync(SETTINGS_PATH)) return null;
fs.mkdirSync(BACKUP_DIR, { recursive: true });
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const dest = path.join(BACKUP_DIR, `settings.claude-pet.${stamp}.json`);
fs.copyFileSync(SETTINGS_PATH, dest);
return dest;
}
function isPetHook(entry) {
try { return JSON.stringify(entry).includes(MARKER); } catch { return false; }
}
function normalize(list) {
if (Array.isArray(list)) return list;
if (list && typeof list === "object") return [list];
return [];
}
function install() {
const settings = readSettings();
const backupPath = backup();
settings.hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
for (const event of EVENTS) {
const kept = normalize(settings.hooks[event]).filter((e) => !isPetHook(e));
kept.push({ hooks: [{ type: "command", command: hookCommand(), timeout: 5 }] });
settings.hooks[event] = kept;
}
writeSettings(settings);
return { events: EVENTS, settingsPath: SETTINGS_PATH, backupPath, command: hookCommand() };
}
function uninstall() {
const settings = readSettings();
if (!settings.hooks || typeof settings.hooks !== "object") return { removed: 0 };
const backupPath = backup();
let removed = 0;
for (const event of Object.keys(settings.hooks)) {
const before = normalize(settings.hooks[event]);
const after = before.filter((e) => !isPetHook(e));
removed += before.length - after.length;
if (after.length) settings.hooks[event] = after;
else delete settings.hooks[event];
}
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
writeSettings(settings);
return { removed, settingsPath: SETTINGS_PATH, backupPath };
}
function isInstalled() {
try {
const settings = readSettings();
return normalize(settings.hooks?.Stop).some(isPetHook);
} catch {
return false;
}
}
module.exports = { install, uninstall, isInstalled, hookCommand, EVENTS, SETTINGS_PATH };