摘要: 加上 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>
114 lines
3.6 KiB
JavaScript
114 lines
3.6 KiB
JavaScript
"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 };
|