#!/usr/bin/env node
// DSH whale-moe theme — idempotent applier for the target DSH install.
// node scripts/apply-theme.mjs # apply to the main install
// node scripts/apply-theme.mjs --target
# apply to a specific install
// node scripts/apply-theme.mjs --rollback
// Every modified file is backed up (original content + SHA-256 manifest) to
// \dsh-whale-moe-\ before the first modification.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import { fileURLToPath, pathToFileURL } from "node:url";
const EXT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const DEFAULT_TARGET = process.env.DSH_INSTALL_DIR || "DeepSeekHarness";
const BACKUP_ROOT = process.env.DSH_WHALE_BACKUP || path.join(os.tmpdir(), "dsh-whale-moe-backup");
const MARKER = "DSH-WHALE-MOE-THEME v1";
const PACK_ID = "whale-moe";
const LABEL = "鲸鱼娘·海洋甜点工房";
const REL = {
indexHtml: "node_modules/@deepseek-ai/dsh-web-frontend/dist/index.html",
themeClient: "node_modules/@deepseek-ai/dsh-client-ui-theme/lib/client.js",
themeHost: "node_modules/@deepseek-ai/dsh-client-ui-theme/lib/index.js"
};
const ASSETS = [
"dsh-whale-moe.css",
"whale-moe-core.js",
"dsh-whale-moe.js",
...["idle-cute", "curious", "running", "thinking", "afk", "waiting", "success", "failure", "teasing",
"blush", "angry", "eat", "star", "celebrate", "sleep", "greet", "night", "wink", "bold", "abstract", "work-pat",
"sweep", "work-slack", "work-ram", "cool-shades", "balance-low",
"work-idea", "work-deadline", "work-boss", "work-slack-phone", "work-sleep",
"meme-smug", "meme-cry", "meme-shock", "meme-broke", "meme-yes", "meme-no", "meme-heart", "meme-music",
"daily-eat", "daily-melt", "daily-shower", "daily-pajama", "daily-coffee", "daily-stretch", "pick-up",
"react-belly", "react-tail", "react-head", "tail-swing",
"weather-umbrella", "weather-rain-happy", "weather-snow", "weather-cold", "weather-thunder",
"levelup", "achievement", "daily-done",
"game-happy", "game-think", "game-cheat", "game-win", "game-lose",
"meme-ojisan", "meme-kyun", "meme-wakuwaku", "meme-doge", "meme-smile-pain", "meme-sike", "meme-omg", "meme-doubt", "meme-worship", "meme-peace",
"festival-spring", "festival-mid-autumn", "festival-halloween", "festival-christmas", "valentine",
"daily-picnic", "daily-cooking", "daily-fishing", "daily-painting", "daily-gaming",
"work-meeting", "work-debug", "work-deploy", "work-review", "work-celebrate"].map((state) => `generated/dsh-whale-state-${state}.webp`),
"generated/dsh-whale-home-peek.webp",
"generated/dsh-whale-workbench-peek.webp",
"generated/dsh-whale-settings-peek.webp",
"peek-calibration.json"
];
function read(file) { return fs.readFileSync(file, "utf8"); }
function write(file, content) { fs.writeFileSync(file, content, "utf8"); }
function sha256(content) { return crypto.createHash("sha256").update(content).digest("hex"); }
function timestamp() { return new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 17); }
function replaceExactlyOnce(source, anchor, replacement, label) {
const first = source.indexOf(anchor);
if (first === -1) throw new Error(`${label}: anchor not found`);
if (source.indexOf(anchor, first + anchor.length) !== -1) throw new Error(`${label}: anchor not unique`);
return source.slice(0, first) + replacement + source.slice(first + anchor.length);
}
function normalizeRel(rel) {
return rel.split(path.sep).join("/");
}
export function patchHost(source) {
if (source.includes(`"${PACK_ID}"`)) return { source, changed: false };
const simple = '\t"whale-maid"\n];';
if (source.includes(simple)) {
return { source: replaceExactlyOnce(source, simple, '\t"whale-maid",\n\t"whale-moe"\n];', "theme host STYLE_PACKS"), changed: true };
}
const extended = '\t"whale-maid",';
if (source.includes(extended)) {
return { source: replaceExactlyOnce(source, extended, '\t"whale-maid",\n\t"whale-moe",', "theme host STYLE_PACKS (extended)"), changed: true };
}
throw new Error("theme host STYLE_PACKS anchor not found");
}
const CLIENT_ANCHORS = [
'{ value: "whale-maid", children: "鲸汐侍礼(鲸鱼娘)" })',
'{ value: "whale-maid", children: "维多利亚航海书房" })'
];
export function patchClient(source) {
if (source.includes('value: "whale-moe"')) return { source, changed: false };
for (const anchor of CLIENT_ANCHORS) {
if (!source.includes(anchor)) continue;
const replacement = `${anchor}, (0, react_jsx_runtime.jsx)("option", { value: "whale-moe", children: ${JSON.stringify(LABEL)} })`;
return { source: replaceExactlyOnce(source, anchor, replacement, "theme client option"), changed: true };
}
throw new Error("theme client option anchor not found");
}
/* ---- mascot-only mode: remove the theme entry but keep the assets ---- */
const MOE_OPTION = `, (0, react_jsx_runtime.jsx)("option", { value: "whale-moe", children: ${JSON.stringify(LABEL)} })`;
export function unpatchClient(source) {
if (!source.includes('value: "whale-moe"')) return { source, changed: false };
if (!source.includes(MOE_OPTION)) throw new Error("theme client whale-moe option not found for removal");
return { source: replaceExactlyOnce(source, MOE_OPTION, "", "theme client option removal"), changed: true };
}
export function unpatchHost(source) {
if (!source.includes('"whale-moe"')) return { source, changed: false };
const tail = '\t"whale-maid",\n\t"whale-moe"\n];';
if (source.includes(tail)) {
return { source: replaceExactlyOnce(source, tail, '\t"whale-maid"\n];', "theme host STYLE_PACKS removal (tail)"), changed: true };
}
const middle = '\t"whale-moe",\n';
if (source.includes(middle)) {
return { source: replaceExactlyOnce(source, middle, "", "theme host STYLE_PACKS removal (middle)"), changed: true };
}
const leading = '\t"whale-moe",\n';
throw new Error("theme host whale-moe entry not found for removal");
}
export function patchIndexHtml(source) {
const marker = ``;
if (source.includes(marker)) return { source, changed: false };
const anchor = '';
const injection = `${anchor}\n ${marker}\n \n \n `;
if (source.includes(anchor)) {
return { source: replaceExactlyOnce(source, anchor, injection, "frontend index theme scripts"), changed: true };
}
console.warn("WARN: victorian script anchor missing; appending before ");
const fallback = ` ${marker}\n \n \n \n `;
return { source: replaceExactlyOnce(source, "", fallback, "frontend index head close"), changed: true };
}
export function planWrites(target, assetsOnly = false) {
const base = path.resolve(target);
const writes = [];
const push = (rel, content) => writes.push({ rel, content });
if (!assetsOnly) {
push(REL.indexHtml, patchIndexHtml(read(path.join(base, ...REL.indexHtml.split("/")))).source);
/* Mascot-only install: no theme-pack option is registered. The pure
patchHost/patchClient/untheme helpers stay exported for legacy installs. */
}
const assetsRoot = path.join(base, "node_modules/@deepseek-ai/dsh-web-frontend/dist/assets");
for (const name of ASSETS) {
const src = path.join(EXT, "assets", name);
if (!fs.existsSync(src)) throw new Error(`theme asset missing: ${src}`);
push(normalizeRel(path.join(path.relative(base, assetsRoot), name)), fs.readFileSync(src));
}
return { target: base, writes };
}
export function apply(target = DEFAULT_TARGET, options = {}) {
const { target: base, writes } = planWrites(target, options.assetsOnly === true);
const backupRoot = typeof options.backupRoot === "string" && options.backupRoot.length > 0 ? options.backupRoot : BACKUP_ROOT;
const records = writes.map((item, index) => {
const dest = path.join(base, ...item.rel.split("/"));
const originalExists = fs.existsSync(dest);
const original = originalExists ? fs.readFileSync(dest) : null;
return {
...item, dest, index,
backupName: originalExists ? `file-${String(index).padStart(2, "0")}-${path.basename(dest)}` : null,
originalExists,
originalSha256: original === null ? null : sha256(original),
patchedSha256: sha256(item.content),
original
};
});
if (records.every((record) => record.originalExists && record.originalSha256 === record.patchedSha256)) {
console.log(`[dsh-whale-moe] ${MARKER} is already applied.`);
return "already";
}
const backupDir = path.join(backupRoot, `dsh-whale-moe-${timestamp()}`);
fs.mkdirSync(backupDir, { recursive: true });
for (const record of records) {
if (record.originalExists) {
fs.mkdirSync(path.dirname(path.join(backupDir, record.backupName)), { recursive: true });
fs.writeFileSync(path.join(backupDir, record.backupName), record.original);
}
}
const manifest = {
marker: MARKER, packId: PACK_ID, target: base, createdAt: new Date().toISOString(),
files: records.map(({ dest, backupName, originalExists, originalSha256, patchedSha256 }) => ({ dest, backupName, originalExists, originalSha256, patchedSha256 }))
};
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
for (const record of records) {
fs.mkdirSync(path.dirname(record.dest), { recursive: true });
fs.writeFileSync(record.dest, record.content);
}
console.log(`[dsh-whale-moe] Applied ${MARKER} to ${base}`);
console.log(`[dsh-whale-moe] Backup: ${backupDir}`);
return backupDir;
}
const UNTHEME_MARKER = "DSH-WHALE-MOE-UNTHEME v1";
export function planUnwrites(target) {
const base = path.resolve(target);
return {
target: base,
writes: [
{ rel: REL.themeClient, content: unpatchClient(read(path.join(base, ...REL.themeClient.split("/")))).source },
{ rel: REL.themeHost, content: unpatchHost(read(path.join(base, ...REL.themeHost.split("/")))).source }
]
};
}
export function untheme(target = DEFAULT_TARGET, options = {}) {
const { target: base, writes } = planUnwrites(target);
const backupRoot = typeof options.backupRoot === "string" && options.backupRoot.length > 0 ? options.backupRoot : BACKUP_ROOT;
const records = writes.map((item, index) => {
const dest = path.join(base, ...item.rel.split("/"));
const originalExists = fs.existsSync(dest);
const original = originalExists ? fs.readFileSync(dest) : null;
return {
...item, dest, index,
backupName: originalExists ? `file-${String(index).padStart(2, "0")}-${path.basename(dest)}` : null,
originalExists,
originalSha256: original === null ? null : sha256(original),
patchedSha256: sha256(item.content),
original
};
});
if (records.every((record) => record.originalExists && record.originalSha256 === record.patchedSha256)) {
console.log(`[dsh-whale-moe] ${UNTHEME_MARKER} is already applied.`);
return "already";
}
const backupDir = path.join(backupRoot, `dsh-whale-moe-untheme-${timestamp()}`);
fs.mkdirSync(backupDir, { recursive: false });
for (const record of records) {
if (record.originalExists) {
fs.mkdirSync(path.dirname(path.join(backupDir, record.backupName)), { recursive: true });
fs.writeFileSync(path.join(backupDir, record.backupName), record.original);
}
}
const manifest = {
marker: UNTHEME_MARKER, target: base, createdAt: new Date().toISOString(),
files: records.map(({ dest, backupName, originalExists, originalSha256, patchedSha256 }) => ({ dest, backupName, originalExists, originalSha256, patchedSha256 }))
};
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
for (const record of records) {
fs.mkdirSync(path.dirname(record.dest), { recursive: true });
fs.writeFileSync(record.dest, record.content);
}
console.log(`[dsh-whale-moe] Un-themed ${base} (mascot assets kept)`);
console.log(`[dsh-whale-moe] Backup: ${backupDir}`);
return backupDir;
}
/* ---- mascot settings section: 鲸鱼娘(鲸鱼娘) ---- */
const MASCOT_SETTINGS_MARKER = "DSH-WHALE-MOE:MASCOT-SETTINGS v27";
const MASCOT_SETTINGS_LEGACY = ["DSH-WHALE-MOE:MASCOT-SETTINGS v1", "DSH-WHALE-MOE:MASCOT-SETTINGS v2", "DSH-WHALE-MOE:MASCOT-SETTINGS v3", "DSH-WHALE-MOE:MASCOT-SETTINGS v4", "DSH-WHALE-MOE:MASCOT-SETTINGS v5", "DSH-WHALE-MOE:MASCOT-SETTINGS v6", "DSH-WHALE-MOE:MASCOT-SETTINGS v7", "DSH-WHALE-MOE:MASCOT-SETTINGS v8", "DSH-WHALE-MOE:MASCOT-SETTINGS v9", "DSH-WHALE-MOE:MASCOT-SETTINGS v10", "DSH-WHALE-MOE:MASCOT-SETTINGS v11", "DSH-WHALE-MOE:MASCOT-SETTINGS v12", "DSH-WHALE-MOE:MASCOT-SETTINGS v13", "DSH-WHALE-MOE:MASCOT-SETTINGS v14", "DSH-WHALE-MOE:MASCOT-SETTINGS v15", "DSH-WHALE-MOE:MASCOT-SETTINGS v16", "DSH-WHALE-MOE:MASCOT-SETTINGS v17", "DSH-WHALE-MOE:MASCOT-SETTINGS v18", "DSH-WHALE-MOE:MASCOT-SETTINGS v19", "DSH-WHALE-MOE:MASCOT-SETTINGS v20", "DSH-WHALE-MOE:MASCOT-SETTINGS v21", "DSH-WHALE-MOE:MASCOT-SETTINGS v22", "DSH-WHALE-MOE:MASCOT-SETTINGS v23", "DSH-WHALE-MOE:MASCOT-SETTINGS v24", "DSH-WHALE-MOE:MASCOT-SETTINGS v25", "DSH-WHALE-MOE:MASCOT-SETTINGS v26"];
const MASCOT_SETTINGS_ANCHOR = "}, ThemePackRow));";
function mascotBlock(marker) {
return `${MASCOT_SETTINGS_ANCHOR}
/* ${marker} */
const mascotReact = require("react");
const MASCOT_NS = "settings.mascot";
const MASCOT_ROW_STYLE = { alignItems: "center", borderBottom: "1px solid var(--dsw-alias-border-l2)", display: "flex", gap: "12px", justifyContent: "space-between", padding: "10px 0" };
const MASCOT_CARD_STYLE = { background: "var(--dsw-alias-bg-module-platform, transparent)", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "14px", display: "flex", flexDirection: "column", gap: "2px", marginTop: "10px", padding: "6px 14px", width: "100%" };
function MascotCard({ title, children }) {
return (0, react_jsx_runtime.jsxs)("div", { style: MASCOT_CARD_STYLE, children: [(0, react_jsx_runtime.jsx)("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", fontWeight: "600", padding: "6px 0 2px" }, children: title }), children] });
}
function MascotValue(key, fallback) {
try { const v = window.localStorage.getItem("whale-moe:" + key); return v === null ? fallback : v; } catch (e) { return fallback; }
}
function MascotPrefRow({ label, prefKey, compact }) {
const [isOn, setIsOn] = mascotReact.useState(MascotValue(prefKey, prefKey === "keywords" ? "0" : "1") !== "0");
return (0, react_jsx_runtime.jsxs)("div", { style: compact ? { alignItems: "center", display: "flex", gap: "8px", justifyContent: "space-between", minWidth: 0, padding: "6px 0" } : MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { style: compact ? { fontSize: "12px", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } : undefined, children: label }), (0, react_jsx_runtime.jsx)("button", {
type: "button", role: "switch",
"aria-checked": isOn,
style: { alignItems: "center", background: isOn ? "var(--dsw-static-accent, #4da3ff)" : "var(--dsw-alias-border-l3, #c9cdd6)", border: "none", borderRadius: "999px", cursor: "pointer", display: "flex", height: "24px", justifyContent: isOn ? "flex-end" : "flex-start", padding: "3px", transition: "background 160ms ease", width: "44px" },
onClick: (event) => {
event.stopPropagation();
event.preventDefault();
const next = !isOn;
try { window.localStorage.setItem("whale-moe:" + prefKey, next ? "1" : "0"); } catch (e) {}
setIsOn(next);
window.dispatchEvent(new CustomEvent("whale-moe-prefs-change", { detail: { key: prefKey, value: next ? "1" : "0" } }));
},
children: (0, react_jsx_runtime.jsx)("span", { style: { background: "#fff", borderRadius: "50%", boxShadow: "0 1px 3px rgb(0 0 0 / 25%)", height: "18px", width: "18px" } })
})] });
}
function MascotTitleRow({ compact }) {
return (0, react_jsx_runtime.jsxs)("label", { style: compact ? { alignItems: "center", display: "flex", gap: "12px", justifyContent: "space-between", padding: "2px 0" } : MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { children: "如何称呼我" }), (0, react_jsx_runtime.jsx)("input", {
type: "text",
defaultValue: MascotValue("title", "主人"),
maxLength: 8,
placeholder: "主人",
style: { flex: 1, maxWidth: "150px", minWidth: 0 },
onChange: (event) => {
try { window.localStorage.setItem("whale-moe:title", event.target.value); } catch (e) {}
window.dispatchEvent(new CustomEvent("whale-moe-prefs-change", { detail: { key: "title", value: event.target.value } }));
}
})] });
}
function MascotWeatherRow() {
const [status, setStatus] = mascotReact.useState("");
const [busy, setBusy] = mascotReact.useState(false);
const save = (key, value) => {
try { window.localStorage.setItem("whale-moe:" + key, value); } catch (e) {}
window.dispatchEvent(new CustomEvent("whale-moe-prefs-change", { detail: { key, value } }));
};
const testNow = () => {
setBusy(true);
setStatus("⏳ 正在连接 Open-Meteo…");
const city = window.localStorage.getItem("whale-moe:weatherCity") || "";
const key = window.localStorage.getItem("whale-moe:weatherKey") || "";
const p = window.DshWhaleMoeWeatherTest ? window.DshWhaleMoeWeatherTest(city, key) : Promise.reject(new Error("天气服务未就绪"));
p.then((text) => { setStatus(text); setBusy(false); }, (error) => {
setStatus("❌ 连接失败:" + (error && error.message ? error.message : "未知错误") + "(无 Key 也可用)");
setBusy(false);
});
};
return (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", width: "100%" }, children: [
(0, react_jsx_runtime.jsxs)("label", { style: MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { children: "天气城市" }), (0, react_jsx_runtime.jsx)("input", {
type: "text",
defaultValue: MascotValue("weatherCity", ""),
placeholder: "如:上海(留空不联网)",
maxLength: 24,
onChange: (event) => save("weatherCity", event.target.value)
})] }),
(0, react_jsx_runtime.jsxs)("label", { style: MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { children: "API Key(选填)" }), (0, react_jsx_runtime.jsx)("input", {
type: "password",
defaultValue: MascotValue("weatherKey", ""),
placeholder: "Open-Meteo 免费无需 Key",
maxLength: 128,
onChange: (event) => save("weatherKey", event.target.value)
})] }),
(0, react_jsx_runtime.jsxs)("div", { style: { ...MASCOT_ROW_STYLE, borderBottom: "none", flexWrap: "wrap" }, children: [
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", lineHeight: "16px", wordBreak: "break-all" }, children: status }),
(0, react_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: testNow, children: busy ? "测试中…" : "测试连接" })
] })
]});
}
function MascotStatRow({ label, value, suffix }) {
return (0, react_jsx_runtime.jsxs)("label", { style: MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { children: label }), (0, react_jsx_runtime.jsx)("span", { children: String(value) + (suffix || "") })] });
}
const MASCOT_ACHIEVEMENTS = [
["first-pat", "🫳", "初次摸头", "第一次摸 鲸鱼娘的头"], ["ten-pats", "🖐️", "摸头十连", "累计摸头 10 次"], ["hundred-pats", "💯", "摸头百连", "累计摸头 100 次"],
["first-feed", "🍰", "投喂成功", "第一次投喂小点心"], ["first-triple", "🎉", "三连击", "触发比心彩蛋"], ["thanks", "💬", "嘴甜", "对 鲸鱼娘说谢谢"],
["lv5", "⭐", "五级", "好感度达到 Lv5"], ["lv10", "👑", "十级", "好感度达到 Lv10"], ["signin3", "📅", "常客", "连续签到 3 天"],
["signin7", "🗓️", "一周之约", "连续签到 7 天"], ["night-owl", "🌙", "深夜陪伴", "深夜互动一次"], ["comeback", "👋", "欢迎回来", "久别重逢"],
["day1", "💞", "一日之缘", "陪伴满 1 天"], ["day7", "💎", "一周相伴", "陪伴满 7 天"], ["day30", "🏛️", "三十日契约", "陪伴满 30 天"],
["first-tool", "🛠️", "开工啦", "第一次工具运行"], ["tools-10", "🔧", "工具十连", "工具运行 10 次"], ["tools-50", "🏭", "工具五十连", "工具运行 50 次"], ["tools-100", "🛰️", "工具百连", "工具运行 100 次"],
["first-code", "💻", "代码初体验", "第一次代码/终端"], ["code-20", "📟", "代码狂人", "代码/终端 20 个"], ["first-success", "✅", "旗开得胜", "第一次任务完成"],
["success-10", "🏆", "任务十连", "任务完成 10 次"], ["first-failure", "🩹", "初次翻车", "第一次任务报错"], ["fail-10", "🚑", "翻车十连", "任务报错 10 次"],
["messages-100", "💌", "会话百条", "会话消息 100 条"], ["messages-500", "📚", "消息五百条", "会话消息 500 条"], ["keyword-master", "🔍", "关键词大师", "关键词互动 10 次"],
["night-work", "🦉", "深夜赶工", "深夜工具仍在运行"], ["balance-low", "🪙", "余额告急", "触发余额不足提醒"],
["game-first", "🫧", "初次开玩", "第一次结算一局小游戏"], ["game-win", "👑", "泡泡之王", "单局戳泡泡得分达到 300"], ["game-combo10", "🔥", "连击达人", "单局最高连击达到 10"], ["game-highscore", "🏆", "纪录刷新", "打破一次历史最高分"],
["quest-first", "🎯", "任务初体验", "完成第一个每日任务"], ["quest-all", "🎟️", "一日全勤", "单日 3 个每日任务全部领取"], ["week-signin7", "🏆", "周常满勤", "本周签到板集满 7 格"],
["bond-action", "🌟", "新动作解锁", "好感度达到 Lv3"], ["bond-badge", "🎖️", "称号首解锁", "好感度达到 Lv5"]
];
function MascotAchievementWall({ ids }) {
const unlocked = ids.length;
return (0, react_jsx_runtime.jsxs)("div", { style: { alignItems: "flex-start", display: "flex", flexDirection: "column", gap: "8px", padding: "4px 0 10px", width: "100%" }, children: [
(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", width: "100%" }, children: [(0, react_jsx_runtime.jsx)("span", { children: "成就墙" }), (0, react_jsx_runtime.jsx)("span", { children: unlocked + " / " + MASCOT_ACHIEVEMENTS.length })] }),
(0, react_jsx_runtime.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(88px, 1fr))", gap: "6px", width: "100%" }, children: MASCOT_ACHIEVEMENTS.map(([id, icon, name]) => {
const on = ids.indexOf(id) !== -1;
return (0, react_jsx_runtime.jsxs)("div", { title: name, style: { alignItems: "center", background: on ? "var(--dsw-alias-interactive-bg-hover)" : "transparent", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "10px", display: "flex", flexDirection: "column", gap: "2px", opacity: on ? 1 : 0.38, padding: "6px 4px", textAlign: "center" }, children: [(0, react_jsx_runtime.jsx)("span", { style: { fontSize: "16px" }, children: icon }), (0, react_jsx_runtime.jsx)("span", { style: { fontSize: "11px", lineHeight: "14px" }, children: name })] });
}) })
]});
}
function MascotBar({ label, value, text, max }) {
const pct = Math.max(0, Math.min(100, Math.round((Number(value) || 0) / (Number(max) || 100) * 100)));
return (0, react_jsx_runtime.jsxs)("div", { style: { alignItems: "center", display: "flex", gap: "10px", padding: "2px 0" }, children: [
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", minWidth: "58px" }, children: label }),
(0, react_jsx_runtime.jsx)("div", { style: { background: "var(--dsw-alias-border-l3, #c9cdd6)", borderRadius: "4px", flex: 1, height: "6px", overflow: "hidden" }, children: (0, react_jsx_runtime.jsx)("div", { style: { background: "var(--dsw-static-accent, #4da3ff)", borderRadius: "4px", height: "100%", transition: "width 160ms ease", width: pct + "%" } }) }),
(0, react_jsx_runtime.jsx)("span", { style: { fontSize: "12px", fontWeight: "600", minWidth: "44px", textAlign: "right" }, children: text })
] });
}
function MascotOverviewCard() {
const [tick, setTick] = mascotReact.useState(0);
mascotReact.useEffect(() => {
const refresh = () => setTick((v) => v + 1);
window.addEventListener("whale-moe-prefs-change", refresh);
window.addEventListener("storage", refresh);
return () => { window.removeEventListener("whale-moe-prefs-change", refresh); window.removeEventListener("storage", refresh); };
}, []);
const mood = MascotValue("mood", "70");
const affinity = MascotValue("affinity", "0");
const level = MascotValue("level", "1");
const streak = MascotValue("signinStreak", "0");
const since = Number(MascotValue("companionSince", ""));
const days = since > 0 ? Math.max(0, Math.floor((Date.now() - since) / 86400000)) : 0;
const chips = [["😊", String(mood), "心情"], ["💗", String(affinity), "好感度"], ["⭐", "Lv." + level, "等级"], ["📅", streak + " 天", "签到"], ["⏳", days + " 天", "陪伴"]];
return (0, react_jsx_runtime.jsxs)("div", { style: { background: "var(--dsw-alias-bg-module-platform, transparent)", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "12px", margin: "10px auto 0", padding: "8px 14px", width: "95%" }, children: [
(0, react_jsx_runtime.jsx)("div", { style: { display: "flex", gap: "6px", width: "100%" }, children: chips.map(([icon, value, label]) => (0, react_jsx_runtime.jsxs)("div", { style: { alignItems: "center", background: "var(--dsw-alias-interactive-bg-hover, transparent)", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "8px", display: "flex", flex: 1, flexDirection: "column", gap: "1px", minWidth: 0, overflow: "hidden", padding: "4px 2px" }, children: [
(0, react_jsx_runtime.jsxs)("span", { style: { fontSize: "11px", fontWeight: "600", lineHeight: "15px", maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [icon, " ", value] }),
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "10px", lineHeight: "13px", maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: label })
] })) }),
(0, react_jsx_runtime.jsx)("div", { style: { background: "var(--dsw-alias-border-l2)", height: "1px", margin: "8px 0 2px" } }),
(0, react_jsx_runtime.jsx)(MascotTitleRow, { compact: true })
] });
}
function MascotWorkSpeechRow() {
const [mode, setMode] = mascotReact.useState(() => (MascotValue("workSpeech", "chatty") === "quiet" ? "quiet" : "chatty"));
const pick = (next) => {
try { window.localStorage.setItem("whale-moe:workSpeech", next); } catch (e) {}
window.dispatchEvent(new CustomEvent("whale-moe-prefs-change", { detail: { key: "workSpeech", value: next } }));
setMode(next);
};
const options = [{ id: "chatty", label: "话痨", hint: "15–40 秒" }, { id: "quiet", label: "安静", hint: "90–240 秒" }];
return (0, react_jsx_runtime.jsxs)("div", { style: { ...MASCOT_ROW_STYLE, borderBottom: "none", flexWrap: "wrap", paddingBottom: 0 }, children: [
(0, react_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 }, children: [
(0, react_jsx_runtime.jsx)("span", { children: "工作中台词" }),
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", lineHeight: "15px" }, children: "按当前任务话题取词;待机问候与天气不受影响" })
] }),
(0, react_jsx_runtime.jsx)("div", { style: { background: "var(--dsw-alias-interactive-bg-hover, transparent)", borderRadius: "10px", display: "flex", gap: "4px", padding: "3px" }, children: options.map((o) => (0, react_jsx_runtime.jsx)("button", {
key: o.id,
type: "button",
"aria-pressed": mode === o.id,
title: "工作中 " + o.hint + "最多说一句",
onClick: (event) => { event.stopPropagation(); event.preventDefault(); pick(o.id); },
style: { background: mode === o.id ? "var(--dsw-static-accent, #4da3ff)" : "transparent", border: "none", borderRadius: "8px", color: mode === o.id ? "#fff" : "inherit", cursor: "pointer", fontSize: "12px", padding: "5px 12px" },
children: o.label
})) })
] });
}
function MascotSwitchGrid() {
const rows = [
{ label: "鲸鱼娘", prefKey: "pet" },
{ label: "台词气泡", prefKey: "chat" },
{ label: "粒子效果", prefKey: "particles" },
{ label: "小游戏", prefKey: "game" },
{ label: "关键词感知", prefKey: "keywords" },
{ label: "摸鱼提醒", prefKey: "idle-nudge" },
{ label: "深夜模式", prefKey: "night" },
{ label: "天气特效", prefKey: "weatherFx" }
];
return (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", width: "100%" }, children: [
(0, react_jsx_runtime.jsx)("div", { style: { display: "grid", gap: "2px 14px", gridTemplateColumns: "1fr 1fr", padding: "2px 0 8px", width: "100%" }, children: rows.map((r) => (0, react_jsx_runtime.jsx)(MascotPrefRow, { key: r.prefKey, label: r.label, prefKey: r.prefKey, compact: true })) }),
(0, react_jsx_runtime.jsx)(MascotWorkSpeechRow, {})
] });
}
function MascotDailyQuests() {
const [tick, setTick] = mascotReact.useState(0);
mascotReact.useEffect(() => {
const refresh = () => setTick((v) => v + 1);
window.addEventListener("whale-moe-prefs-change", refresh);
window.addEventListener("storage", refresh);
return () => { window.removeEventListener("whale-moe-prefs-change", refresh); window.removeEventListener("storage", refresh); };
}, []);
const pool = (window.DshWhaleMoeCore && window.DshWhaleMoeCore.QUEST_POOL) || [];
const defOf = (id) => pool.find((q) => q.id === id) || { id, desc: id, target: 1, reward: { affinity: 0, mood: 0 } };
let quests = null;
try { const raw = window.localStorage.getItem("whale-moe:quests"); quests = raw ? JSON.parse(raw) : null; } catch (e) { quests = null; }
const today = (() => { const d = new Date(); return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate(); })();
const slots = quests && quests.date === today && Array.isArray(quests.slots) ? quests.slots : [];
const claim = (id) => {
try { if (window.__dshWhaleMoeClaimQuest) window.__dshWhaleMoeClaimQuest(id); } catch (e) {}
setTick((v) => v + 1);
};
if (!slots.length) return (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "4px 0" }, children: "今日任务加载中,稍后自动刷新" });
return (0, react_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", padding: "2px 0 4px", width: "100%" }, children: slots.map((slot, index) => {
const def = defOf(slot.id);
const done = slot.progress >= def.target;
const last = index === slots.length - 1;
return (0, react_jsx_runtime.jsxs)("div", { style: { alignItems: "center", borderBottom: last ? "none" : "1px solid var(--dsw-alias-border-l2)", display: "flex", gap: "10px", minHeight: "44px" }, children: [
(0, react_jsx_runtime.jsx)("span", { style: { flex: 1, fontSize: "13px", lineHeight: "18px", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: def.desc }),
(0, react_jsx_runtime.jsxs)("div", { style: { alignItems: "center", display: "flex", gap: "6px", width: "104px" }, children: [
(0, react_jsx_runtime.jsx)("div", { style: { background: "var(--dsw-alias-border-l3, #c9cdd6)", borderRadius: "4px", flex: 1, height: "7px", overflow: "hidden" }, children: (0, react_jsx_runtime.jsx)("div", { style: { background: done ? "var(--dsw-static-accent, #4da3ff)" : "var(--dsw-alias-label-secondary, #888)", borderRadius: "4px", height: "100%", transition: "width 160ms ease", width: Math.min(100, Math.round(slot.progress / def.target * 100)) + "%" } }) }),
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", lineHeight: "14px", whiteSpace: "nowrap" }, children: String(Math.min(slot.progress, def.target)) + "/" + String(def.target) })
] }),
slot.claimed
? (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", textAlign: "right", width: "58px" }, children: "✅ 已领" })
: (0, react_jsx_runtime.jsx)("button", { type: "button", disabled: !done, onClick: () => claim(slot.id), style: { background: done ? "var(--dsw-static-accent, #4da3ff)" : "transparent", border: "1px solid var(--dsw-alias-border-l3, #c9cdd6)", borderRadius: "8px", color: done ? "#fff" : "var(--dsw-alias-label-secondary)", cursor: done ? "pointer" : "default", fontSize: "12px", height: "26px", padding: "0 10px", width: "58px" }, children: "领取" })
] });
}) });
}
function MascotWeekSignin() {
const [tick, setTick] = mascotReact.useState(0);
mascotReact.useEffect(() => {
const refresh = () => setTick((v) => v + 1);
window.addEventListener("whale-moe-prefs-change", refresh);
return () => window.removeEventListener("whale-moe-prefs-change", refresh);
}, []);
let week = null;
try { const raw = window.localStorage.getItem("whale-moe:weekSignin"); week = raw ? JSON.parse(raw) : null; } catch (e) { week = null; }
const days = week && Array.isArray(week.days) ? week.days.length : 0;
const labels = ["一", "二", "三", "四", "五", "六", "日"];
return (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 0 10px", width: "100%" }, children: [
(0, react_jsx_runtime.jsx)("div", { style: { display: "flex", gap: "4px" }, children: Array.from({ length: 7 }, (_, i) => (0, react_jsx_runtime.jsxs)("div", { style: { alignItems: "center", display: "flex", flex: 1, flexDirection: "column", gap: "4px" }, children: [
(0, react_jsx_runtime.jsx)("span", { style: { alignItems: "center", background: i < days ? "var(--dsw-static-accent, #4da3ff)" : "var(--dsw-alias-border-l3, #c9cdd6)", borderRadius: "50%", color: i < days ? "#fff" : "transparent", display: "flex", fontSize: "12px", height: "26px", justifyContent: "center", width: "26px" }, children: "✓" }),
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", lineHeight: "14px" }, children: labels[i] })
] })) }),
(0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", lineHeight: "16px" }, children: "本周已签到 " + days + " / 7 天 · 集满 1/3/7 天有里程碑奖励" })
] });
}
function MascotBadgeRow() {
const [tick, setTick] = mascotReact.useState(0);
mascotReact.useEffect(() => {
const refresh = () => setTick((v) => v + 1);
window.addEventListener("whale-moe-prefs-change", refresh);
return () => window.removeEventListener("whale-moe-prefs-change", refresh);
}, []);
const level = Number(MascotValue("level", "1")) || 1;
const badges = (window.DshWhaleMoeCore && window.DshWhaleMoeCore.BOND && window.DshWhaleMoeCore.BOND.badges) || [];
const unlocked = badges.filter((b) => level >= b.minLevel);
const current = MascotValue("badge", "");
if (!unlocked.length) return (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "4px 0" }, children: "未解锁(好感度 Lv5 解锁首个称号)" });
return (0, react_jsx_runtime.jsxs)("label", { style: { ...MASCOT_ROW_STYLE, borderBottom: "none" }, children: [(0, react_jsx_runtime.jsx)("span", { children: "称号" }), (0, react_jsx_runtime.jsx)("select", {
value: current,
onChange: (event) => { const value = event.target.value; try { if (window.__dshWhaleMoeApplyBadge) window.__dshWhaleMoeApplyBadge(value); else window.localStorage.setItem("whale-moe:badge", value); } catch (e) {} setTick((v) => v + 1); },
children: [(0, react_jsx_runtime.jsx)("option", { value: "", children: "(不使用称号)" })].concat(unlocked.map((b) => (0, react_jsx_runtime.jsx)("option", { value: b.id, children: b.name })))
})] });
}
function MascotAccordion({ title, icon, summary, defaultOpen, children }) {
const [open, setOpen] = mascotReact.useState(!!defaultOpen);
const left = (0, react_jsx_runtime.jsxs)("span", { style: { alignItems: "center", display: "flex", gap: "8px", minWidth: 0 }, children: [
(0, react_jsx_runtime.jsx)("span", { style: { fontSize: "14px" }, children: icon || "" }),
(0, react_jsx_runtime.jsxs)("span", { style: { alignItems: "flex-start", display: "flex", flexDirection: "column", minWidth: 0 }, children: [
(0, react_jsx_runtime.jsx)("span", { style: { fontSize: "13px", fontWeight: "600" }, children: title }),
summary ? (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", fontWeight: "400" }, children: summary }) : null
] })
] });
const right = (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px" }, children: open ? "▾" : "▸" });
return (0, react_jsx_runtime.jsxs)("div", { style: { background: "var(--dsw-alias-bg-module-platform, transparent)", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "12px", marginTop: "10px", overflow: "hidden", width: "100%" }, children: [
(0, react_jsx_runtime.jsxs)("button", { type: "button", onClick: () => setOpen(!open), style: { alignItems: "center", background: "transparent", border: "none", color: "inherit", cursor: "pointer", display: "flex", justifyContent: "space-between", padding: "10px 14px", width: "100%" }, children: [left, right] }),
open ? (0, react_jsx_runtime.jsx)("div", { style: { padding: "0 14px 8px" }, children }) : null
] });
}
function MascotTabs({ active, onChange, tabs }) {
const list = tabs || [{ id: "quests", label: "今日任务" }, { id: "week", label: "本周签到" }];
return (0, react_jsx_runtime.jsx)("div", { style: { background: "var(--dsw-alias-interactive-bg-hover, transparent)", borderRadius: "10px", display: "flex", gap: "4px", marginBottom: "8px", padding: "3px" }, children: list.map((t) => (0, react_jsx_runtime.jsx)("button", { key: t.id, type: "button", onClick: () => onChange(t.id), style: { background: active === t.id ? "var(--dsw-static-accent, #4da3ff)" : "transparent", border: "none", borderRadius: "8px", color: active === t.id ? "#fff" : "inherit", cursor: "pointer", flex: 1, fontSize: "13px", padding: "6px 0" }, children: t.label })) });
}
function MascotDailyCard() {
const [tab, setTab] = mascotReact.useState("quests");
return (0, react_jsx_runtime.jsxs)(MascotAccordion, { title: "日常与养成", icon: "🎯", summary: "任务、签到与称号", defaultOpen: false, children: [
(0, react_jsx_runtime.jsx)(MascotTabs, { active: tab, onChange: setTab, tabs: [{ id: "quests", label: "今日任务" }, { id: "week", label: "本周签到" }, { id: "badge", label: "称号" }] }),
tab === "quests" ? (0, react_jsx_runtime.jsx)(MascotDailyQuests, {}) : (tab === "week" ? (0, react_jsx_runtime.jsx)(MascotWeekSignin, {}) : (0, react_jsx_runtime.jsx)(MascotBadgeRow, {}))
] });
}
function MascotAchievementRow() {
const ids = MascotValue("achievements", "").split(",").filter(Boolean);
return (0, react_jsx_runtime.jsx)(MascotAchievementWall, { ids });
}
function MascotResetRow() {
return (0, react_jsx_runtime.jsxs)("label", { style: MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { children: "悬浮位置" }), (0, react_jsx_runtime.jsx)("button", { type: "button", onClick: () => { try { window.localStorage.removeItem("whale-moe:floatX"); window.localStorage.removeItem("whale-moe:floatY"); } catch (e) {} window.dispatchEvent(new CustomEvent("whale-moe-prefs-change", { detail: { key: "float-reset", value: true } })); }, children: "重置到默认位置" })] });
}
function MascotGrowthResetRow() {
return (0, react_jsx_runtime.jsxs)("label", { style: MASCOT_ROW_STYLE, children: [(0, react_jsx_runtime.jsx)("span", { children: "养成数据" }), (0, react_jsx_runtime.jsx)("button", { type: "button", onClick: () => { ["mood", "affinity", "satiety", "lastSignin", "signinStreak", "achievements", "companionSince", "level", "quests", "weekSignin", "badge", "gameStats"].forEach((k) => { try { window.localStorage.removeItem("whale-moe:" + k); } catch (e) {} }); window.dispatchEvent(new CustomEvent("whale-moe-prefs-change", { detail: { key: "growth-reset", value: true } })); }, children: "重置养成" })] });
}
function MascotPrefRows() {
return (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", width: "100%" }, children: [
(0, react_jsx_runtime.jsx)(MascotOverviewCard, {}),
(0, react_jsx_runtime.jsxs)(MascotAccordion, { title: "陪伴表现", icon: "🎛️", summary: "鲸鱼娘怎么出现、怎么说话", defaultOpen: true, children: [(0, react_jsx_runtime.jsx)(MascotSwitchGrid, {})] }),
(0, react_jsx_runtime.jsx)(MascotAccordion, { title: "天气", icon: "⛅", summary: "城市与天气特效", defaultOpen: false, children: [(0, react_jsx_runtime.jsx)(MascotWeatherRow, {}), (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", lineHeight: "15px", padding: "0 0 6px" }, children: "特效需已填写城市、且天气数据新鲜时才会显示;工作繁忙时自动减弱,雷闪停播。" })] }),
(0, react_jsx_runtime.jsx)(MascotDailyCard, {}),
(0, react_jsx_runtime.jsx)(MascotAccordion, { title: "成就墙", icon: "🏅", summary: "已解锁 " + MascotValue("achievements", "").split(",").filter(Boolean).length + " / " + MASCOT_ACHIEVEMENTS.length, defaultOpen: false, children: [(0, react_jsx_runtime.jsx)(MascotAchievementRow, {})] }),
(0, react_jsx_runtime.jsx)(MascotAccordion, { title: "数据与重置", icon: "🗂️", summary: "位置与养成数据", defaultOpen: false, children: [(0, react_jsx_runtime.jsx)(MascotResetRow, {}), (0, react_jsx_runtime.jsx)(MascotGrowthResetRow, {})] })
]});
}
function MascotSection({ renderSlot }) {
return (0, react_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", width: "100%" }, children: renderSlot("settings.mascot.item", {}) });
}
ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "mascot", order: 6, label: "看板娘", children: { "settings.mascot.item": { kind: "list", scope: "root" } } }, MascotSection));
ctx.slots.inject("settings.mascot.item", () => ctx.slots.register({ name: "settings.mascot.item", id: "mascot-prefs", order: 0, store, locale: MASCOT_NS, inject: injected }, MascotPrefRows));`;
}
export function patchMascotClient(source) {
if (source.includes(MASCOT_SETTINGS_MARKER)) return { source, changed: false };
const legacyMarker = MASCOT_SETTINGS_LEGACY.find((marker) => source.includes(`/* ${marker} */`));
if (legacyMarker) {
const start = source.indexOf(`/* ${legacyMarker} */`);
const tail = "}, MascotPrefRows));";
const end = source.indexOf(tail, start);
if (start === -1 || end === -1) throw new Error("legacy mascot settings block not found for upgrade");
const withoutLegacy = source.slice(0, start) + source.slice(end + tail.length);
return { source: replaceExactlyOnce(withoutLegacy, MASCOT_SETTINGS_ANCHOR, mascotBlock(MASCOT_SETTINGS_MARKER), "mascot settings slot upgrade"), changed: true };
}
return { source: replaceExactlyOnce(source, MASCOT_SETTINGS_ANCHOR, mascotBlock(MASCOT_SETTINGS_MARKER), "mascot settings slot"), changed: true };
}
export function planMascotWrites(target) {
const base = path.resolve(target);
return {
target: base,
writes: [
{ rel: REL.themeClient, content: patchMascotClient(read(path.join(base, ...REL.themeClient.split("/")))).source }
]
};
}
export function mascotSettings(target = DEFAULT_TARGET, options = {}) {
const { target: base, writes } = planMascotWrites(target);
const backupRoot = typeof options.backupRoot === "string" && options.backupRoot.length > 0 ? options.backupRoot : BACKUP_ROOT;
const records = writes.map((item, index) => {
const dest = path.join(base, ...item.rel.split("/"));
const originalExists = fs.existsSync(dest);
const original = originalExists ? fs.readFileSync(dest) : null;
return {
...item, dest, index,
backupName: originalExists ? `file-${String(index).padStart(2, "0")}-${path.basename(dest)}` : null,
originalExists,
originalSha256: original === null ? null : sha256(original),
patchedSha256: sha256(item.content),
original
};
});
if (records.every((record) => record.originalExists && record.originalSha256 === record.patchedSha256)) {
console.log(`[dsh-whale-moe] ${MASCOT_SETTINGS_MARKER} is already applied.`);
return "already";
}
const backupDir = path.join(backupRoot, `dsh-whale-moe-mascot-settings-${timestamp()}`);
fs.mkdirSync(backupDir, { recursive: false });
for (const record of records) {
if (record.originalExists) {
fs.mkdirSync(path.dirname(path.join(backupDir, record.backupName)), { recursive: true });
fs.writeFileSync(path.join(backupDir, record.backupName), record.original);
}
}
const manifest = {
marker: MASCOT_SETTINGS_MARKER, target: base, createdAt: new Date().toISOString(),
files: records.map(({ dest, backupName, originalExists, originalSha256, patchedSha256 }) => ({ dest, backupName, originalExists, originalSha256, patchedSha256 }))
};
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
for (const record of records) {
fs.mkdirSync(path.dirname(record.dest), { recursive: true });
fs.writeFileSync(record.dest, record.content);
}
console.log(`[dsh-whale-moe] Installed mascot settings section (${base})`);
console.log(`[dsh-whale-moe] Backup: ${backupDir}`);
return backupDir;
}
export function rollback(backupDir) {
const manifestPath = path.join(backupDir, "manifest.json");
if (!fs.existsSync(manifestPath)) throw new Error(`Backup manifest not found: ${manifestPath}`);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
if (![MARKER, UNTHEME_MARKER, MASCOT_SETTINGS_MARKER, MASCOT_SETTINGS_LEGACY].includes(manifest.marker) || !Array.isArray(manifest.files)) throw new Error(`Unsupported backup manifest: ${manifestPath}`);
for (const record of manifest.files) {
if (record.originalExists) {
const backup = fs.readFileSync(path.join(backupDir, record.backupName));
if (sha256(backup) !== record.originalSha256) throw new Error(`Backup checksum mismatch: ${record.dest}`);
fs.mkdirSync(path.dirname(record.dest), { recursive: true });
fs.writeFileSync(record.dest, backup);
} else if (fs.existsSync(record.dest)) {
fs.unlinkSync(record.dest);
}
}
console.log(`[dsh-whale-moe] Rolled back: ${backupDir}`);
return backupDir;
}
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isDirectRun) {
const args = process.argv.slice(2);
const targetIdx = args.indexOf("--target");
const rollbackIdx = args.indexOf("--rollback");
const unthemeIdx = args.indexOf("--untheme");
const assetsOnlyIdx = args.indexOf("--assets-only");
const mascotSettingsIdx = args.indexOf("--mascot-settings");
if (rollbackIdx >= 0) {
const dir = args[rollbackIdx + 1];
if (!dir) { console.error("usage: --rollback "); process.exit(2); }
rollback(path.resolve(dir));
} else if (unthemeIdx >= 0) {
const target = targetIdx >= 0 ? args[targetIdx + 1] : DEFAULT_TARGET;
if (!target) { console.error("usage: --untheme [--target ]"); process.exit(2); }
if (!fs.existsSync(target)) { console.error(`target not found: ${target}`); process.exit(2); }
untheme(target);
} else if (mascotSettingsIdx >= 0) {
const target = targetIdx >= 0 ? args[targetIdx + 1] : DEFAULT_TARGET;
if (!target) { console.error("usage: --mascot-settings [--target ]"); process.exit(2); }
if (!fs.existsSync(target)) { console.error(`target not found: ${target}`); process.exit(2); }
mascotSettings(target);
} else {
const target = targetIdx >= 0 ? args[targetIdx + 1] : DEFAULT_TARGET;
if (!target) { console.error("usage: [--target ]"); process.exit(2); }
if (!fs.existsSync(target)) { console.error(`target not found: ${target}`); process.exit(2); }
apply(target, { assetsOnly: assetsOnlyIdx >= 0 });
}
}