/** * 把 client.js 改成右侧 dsh-purge 双页签(清洗 + 演练台),并嵌入 vendor 演练台 UI。 * node scripts/patch-client-dock.mjs */ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const clientPath = join(root, "client.js"); const legalPath = join(root, "docs", "drill-auth-legal.html"); const BEGIN = "/* __DSH_PURGE_DRILL_BEGIN__ */"; const END = "/* __DSH_PURGE_DRILL_END__ */"; const LEGAL = readFileSync(legalPath, "utf8").trim(); let client = readFileSync(clientPath, "utf8").replace(/\r\n/g, "\n"); // 1) locale keys if (!client.includes('"dock.clean"')) { client = client.replace( '\t\t\tnav: "规则设定",', `\t\t\tnav: "dsh-purge", "dock.clean": "清洗", "dock.drill": "演练台", "dock.toggle": "dsh-purge", "dock.newSession": "dsh-purge", "dock.full": "全面浏览", "dock.close": "收起", "dock.unauthorized": "未授权", "auth.title": "演练台授权确认", "auth.lead": "清洗不需要这一步。第一次进入演练台,或打开全面浏览,都要先完成本窗。", "auth.warn": "安全警告:演练台只用于你有权管理的本机、离线靶标,或已书面授权的演练环境。未经授权的渗透、攻击、窃取和破坏一律禁止。", "auth.hint.wait": "请阅读声明。{n} 秒后才能勾选。", "auth.hint.scroll": "倒计时已结束。请把声明滚到文末,再勾选。", "auth.hint.check": "倒计时已结束。请勾选全部三项。", "auth.check.read": "我已读完《安全与禁止违法声明》,并认可其中的全部条款。", "auth.check.scope": "我确认只用在本机已授权环境、离线靶标或合规演练中。", "auth.check.ban": "我确认禁止任何违法用途。未完成本授权,不得使用演练台。", "auth.no": "暂不进入", "auth.ok": "确认授权", "auth.ok.tab": "确认授权并进入演练台", "auth.ok.full": "确认授权并全面浏览",`, ); client = client.replace( '\t\t\tnav: "Rules",', `\t\t\tnav: "dsh-purge", "dock.clean": "Clean", "dock.drill": "Drill", "dock.toggle": "dsh-purge", "dock.newSession": "dsh-purge", "dock.full": "Full view", "dock.close": "Collapse", "dock.unauthorized": "Unauthorized", "auth.title": "Drill console authorization", "auth.lead": "Cleaning does not need this step. Opening Drill or Full view the first time requires this dialog.", "auth.warn": "Warning: the drill console is only for hosts you manage, offline targets, or written authorized exercises. Unauthorized intrusion, attack, theft, or sabotage is forbidden.", "auth.hint.wait": "Read the notice. You can check the boxes in {n}s.", "auth.hint.scroll": "Countdown finished. Scroll to the end, then check the boxes.", "auth.hint.check": "Countdown finished. Check all three boxes.", "auth.check.read": "I have read the Safety and Anti-Abuse Notice and accept every clause.", "auth.check.scope": "I will only use authorized local hosts, offline targets, or compliant exercises.", "auth.check.ban": "I will not use this for any illegal purpose. Without this authorization the drill console stays closed.", "auth.no": "Not now", "auth.ok": "Authorize", "auth.ok.tab": "Authorize and open Drill", "auth.ok.full": "Authorize and open Full view",`, ); } // 2) dock CSS into PURGE_CSS (append before closing backtick of PURGE_CSS) if (!client.includes(".dshp-dock{")) { const cssExtra = ` .dshp-dock{position:fixed;z-index:10050;display:flex;flex-direction:column;width:var(--dshp-dock-w,620px);height:var(--dshp-dock-h,72vh);min-width:380px;min-height:320px;max-width:calc(100vw - 16px);max-height:calc(100vh - 16px);box-sizing:border-box;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1,var(--dshp-paper,#1c1c1c)) 52%,transparent);backdrop-filter:blur(22px) saturate(1.35);-webkit-backdrop-filter:blur(22px) saturate(1.35);border:1px solid color-mix(in srgb,var(--dsw-alias-border-l1,var(--dshp-line,#555)) 65%,transparent);border-radius:12px;box-shadow:0 18px 48px color-mix(in srgb,#000 30%,transparent),0 0 0 1px color-mix(in srgb,#fff 6%,transparent);pointer-events:auto;color:var(--dsw-alias-label-primary,var(--dshp-ink,#f2f2f2));overflow:hidden} .dshp-dock[data-open="0"]{display:none} .dshp-dock-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid color-mix(in srgb,var(--dsw-alias-border-l1,var(--dshp-line,#555)) 55%,transparent);background:transparent;cursor:grab;user-select:none;touch-action:none;color:var(--dsw-alias-label-primary,var(--dshp-ink,#f2f2f2))} .dshp-dock-head:active{cursor:grabbing} .dshp-dock-head b{font-size:14px;font-weight:600;color:inherit;text-shadow:0 1px 0 color-mix(in srgb,#000 18%,transparent)} .dshp-dock-head button{cursor:pointer;color:inherit;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,rgba(127,127,127,.18)) 55%,transparent)} .dshp-dock-tabs{display:flex;gap:4px;padding:0 12px;border-bottom:1px solid color-mix(in srgb,var(--dsw-alias-border-l1,var(--dshp-line,#555)) 55%,transparent);background:transparent} .dshp-dock-tab{appearance:none;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary,var(--dshp-mute,#b8b4ac));border-radius:0;padding:10px 14px 8px;margin-bottom:-1px;cursor:pointer;font:13px/1.2 inherit;text-shadow:0 1px 0 color-mix(in srgb,#000 12%,transparent)} .dshp-dock-tab:hover{color:var(--dsw-alias-label-primary,var(--dshp-ink,#f2f2f2))} .dshp-dock-tab.on{color:var(--dsw-alias-label-primary,var(--dshp-ink,#f2f2f2));font-weight:600;border-bottom-color:var(--dsw-alias-brand-primary,#6dbf8c);background:transparent} .dshp-dock-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;background:transparent} .dshp-dock-body .dshp-root{max-width:none;height:100%;flex:1;min-height:0;overflow:auto;background:transparent!important;--dshp-bg:transparent;--dshp-paper:color-mix(in srgb,var(--dshp-ink) 7%,transparent);--dshp-fill:color-mix(in srgb,var(--dshp-ink) 6%,transparent);--dshp-line:color-mix(in srgb,var(--dshp-ink) 20%,transparent);--dshp-accent-soft:color-mix(in srgb,var(--dshp-accent) 22%,transparent)} .dshp-dock-body .dshp-root[data-theme="white"]{--dshp-ink:#1c1b18;--dshp-mute:#5a554c;--dshp-ok:#2f6b45;--dshp-warn:#8a5f18;--dshp-bad:#a14545} .dshp-dock-body .dshp-root[data-theme="dusk"]{--dshp-ink:#f4f1ea;--dshp-mute:#c9c3b8;--dshp-ok:#9fd0ad;--dshp-warn:#e0c08a;--dshp-bad:#e0b0b0} .dshp-dock-body .dshp-panel,.dshp-dock-body .dshp-metric,.dshp-dock-body .dshp-group,.dshp-dock-body .dshp-editor,.dshp-dock-body .dshp-active,.dshp-dock-body .dshp-ask,.dshp-dock-body .dshp-create,.dshp-dock-body .dshp-switch{background:color-mix(in srgb,var(--dshp-ink) 6%,transparent)!important} .dshp-dock-body .dshp-field,.dshp-dock-body .dshp-area{background:color-mix(in srgb,var(--dshp-ink) 8%,transparent);color:var(--dshp-ink)} .dshp-dock-body .dshp-title,.dshp-dock-body .dshp-kicker,.dshp-dock-body .dshp-sub h4,.dshp-dock-body .dshp-group-h strong,.dshp-dock-body .dshp-metric b{color:var(--dshp-ink)} .dshp-dock-body .dshp-mute,.dshp-dock-body .dshp-metric span,.dshp-dock-body .dshp-group-h em,.dshp-dock-body .dshp-count,.dshp-dock-body .dshp-rule-meta{color:var(--dshp-mute)} .dshp-dock-body .rt-dock{position:relative;inset:auto;width:100%!important;height:100%;flex:1;min-height:0;display:flex!important;box-shadow:none;border:0;transform:none!important;opacity:1!important;pointer-events:auto!important;background:transparent!important;color:var(--dsw-alias-label-primary,#f2f2f2)} .dshp-dock-body .rt-grip{display:none!important} .dshp-dock-body .rt-embedded>.rt-head,.dshp-dock-body .rt-tabs,.dshp-dock-body .rt-body,.dshp-dock-body .rt-foot,.dshp-dock-body .rt-card,.dshp-dock-body .rt-pane,.dshp-dock-body .rt-side,.dshp-dock-body .rt-main{background:transparent!important;color:inherit} .dshp-dock-body .rt-title,.dshp-dock-body .rt-tab.on,.dshp-dock-body .rt-btn{color:var(--dsw-alias-label-primary,#f2f2f2)} .dshp-dock-body .rt-tab{color:var(--dsw-alias-label-secondary,#c2bdb4)} .dshp-dock-body .rt-embedded>.rt-head{padding:8px 12px} .dshp-dock-body .rt-embedded>.rt-head .rt-title{font-size:13px} .dshp-dock-body .rt-tabs{gap:2px;padding:6px 12px 0} .dshp-dock-body .rt-tab{border-radius:0;border-bottom:2px solid transparent;margin-bottom:-1px;padding:8px 10px 7px;background:transparent} .dshp-dock-body .rt-tab.on{border-bottom-color:var(--dsw-alias-brand-primary,#6dbf8c);background:transparent;font-weight:600} .dshp-dock-body .rt-btn,.dshp-dock-body .rt-input{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,rgba(127,127,127,.2)) 60%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-border-l1,#666) 70%,transparent);color:inherit} .dshp-dock-resize{position:absolute;right:2px;bottom:2px;width:18px;height:18px;cursor:nwse-resize;z-index:3;background:linear-gradient(135deg,transparent 46%,color-mix(in srgb,var(--dsw-alias-label-secondary,#999) 70%,transparent) 46%);border-radius:2px;opacity:.85} .dshp-dock-resize-l{position:absolute;left:0;top:28px;bottom:18px;width:6px;cursor:ew-resize;z-index:3;background:transparent} .dshp-dock-resize-l:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#6dbf8c) 35%,transparent)} .dshp-auth-mask{position:fixed;inset:0;z-index:10100;background:rgba(0,0,0,.62);display:flex;align-items:center;justify-content:center;padding:20px;pointer-events:auto} .dshp-auth-modal{width:min(680px,100%);max-height:min(88vh,820px);display:flex;flex-direction:column;gap:10px;padding:16px;border:1px solid var(--dsw-alias-border-l1,#333);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#1a1a1a);color:var(--dsw-alias-label-primary,#eee)} .dshp-auth-modal h2{margin:0;font-size:16px;font-weight:600} .dshp-auth-warn{padding:8px 10px;border-radius:8px;background:#3a2e18;color:#f0d48a;font-size:13px} .dshp-auth-legal{overflow:auto;height:300px;margin:0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1,#333);border-radius:8px;background:#121212;font-size:13px;line-height:1.55} .dshp-auth-legal h3{margin:14px 0 4px;font-size:13px} .dshp-auth-legal h3:first-child{margin-top:0} .dshp-auth-legal p{margin:0 0 8px} .dshp-auth-checks{display:flex;flex-direction:column;gap:8px;font-size:13px} .dshp-auth-checks label{display:flex;gap:8px;align-items:flex-start} .dshp-auth-checks label.locked{opacity:.45} .dshp-auth-ops{display:flex;justify-content:flex-end;gap:8px} .dshp-auth-ops button,.dshp-dock-head button{appearance:none;border:1px solid var(--dsw-alias-border-l1,#444);background:transparent;color:inherit;border-radius:8px;padding:6px 12px;cursor:pointer;font:inherit} .dshp-auth-ops button.primary{background:#2a4033;border-color:#8fbf9a} .dshp-auth-ops button:disabled{opacity:.45;cursor:default} .dshp-icon-btn{appearance:none;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;font:13px/1 inherit} .dshp-icon-btn.on,.dshp-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,currentColor)} .dshp-hbtn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;font:12px/1 inherit;padding:4px 8px;border-radius:8px} .dshp-hbtn.on,.dshp-hbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,currentColor)} `; // Find end of PURGE_CSS - the backtick after Rewind starts at REWIND_CSS const purgeCssEnd = client.indexOf("\t\tconst REWIND_CSS"); if (purgeCssEnd < 0) throw new Error("REWIND_CSS not found"); // PURGE_CSS ends just before that - look backward for closing ` const before = client.lastIndexOf("`;", purgeCssEnd); if (before < 0) throw new Error("PURGE_CSS end not found"); client = client.slice(0, before) + cssExtra + client.slice(before); } // 3) Insert dock/auth/marker block before installRewindUi const dockBlock = ` const AUTH_KEY = "dsh-purge-drill-auth"; const AUTH_LEGAL_HTML = ${JSON.stringify(LEGAL)}; ${BEGIN} const __dshPurgeDrill = { CSS: "", Panel: null, setUI: () => {}, useUI: () => ({ open: false }), isFullWindow: () => false, setDockWidth: () => {} }; ${END} function readDrillAuth() { try { return window.localStorage.getItem(AUTH_KEY) === "1"; } catch { return false; } } function writeDrillAuth(ok) { try { if (ok) window.localStorage.setItem(AUTH_KEY, "1"); else window.localStorage.removeItem(AUTH_KEY); } catch { /* ignore */ } } let dockBus = { open: false, tab: "clean", authOpen: false, pending: "tab", listeners: new Set() }; function getDock() { return dockBus; } function setDock(patch) { dockBus = Object.assign({}, dockBus, patch); dockBus.listeners.forEach((fn) => { try { fn(dockBus); } catch { /* ignore */ } }); } function useDock() { const [st, setSt] = useState(dockBus); useEffect(() => { const fn = (next) => setSt(next); dockBus.listeners.add(fn); return () => { dockBus.listeners.delete(fn); }; }, []); return st; } function requestDrill(kind) { const pending = kind || "tab"; if (readDrillAuth()) { setDock({ open: true, tab: "drill", authOpen: false, pending: "tab" }); try { if (__dshPurgeDrill && __dshPurgeDrill.setUI) __dshPurgeDrill.setUI({ open: true }); } catch { /* ignore */ } if (pending === "full") { try { window.open(window.location.href.split("#")[0] + "#redteam-full", "_blank", "noopener"); } catch { /* ignore */ } } return; } setDock({ open: true, tab: "drill", authOpen: true, pending: pending }); } function openDockClean() { setDock({ open: true, tab: "clean", authOpen: false, pending: "tab" }); } function toggleDock() { if (dockBus.open) setDock({ open: false, authOpen: false, pending: "tab" }); else openDockClean(); } function AuthGate(props) { const t = useT(); const [left, setLeft] = useState(10); const [readEnd, setReadEnd] = useState(false); const [checks, setChecks] = useState({ read: false, scope: false, ban: false }); const legalRef = useRef(null); useEffect(() => { const id = window.setInterval(() => { setLeft((n) => { if (n <= 1) { window.clearInterval(id); return 0; } return n - 1; }); }, 1000); return () => window.clearInterval(id); }, []); useEffect(() => { const el = legalRef.current; if (!el) return; if (el.scrollHeight <= el.clientHeight + 4) setReadEnd(true); }, []); const locked = left > 0 || !readEnd; const ready = !locked && checks.read && checks.scope && checks.ban; const hint = left > 0 ? t("auth.hint.wait").replace("{n}", String(left)) : (readEnd ? t("auth.hint.check") : t("auth.hint.scroll")); const onScroll = () => { const el = legalRef.current; if (!el) return; if (el.scrollTop + el.clientHeight >= el.scrollHeight - 8) setReadEnd(true); }; const toggle = (id) => { if (locked) return; setChecks((prev) => Object.assign({}, prev, { [id]: !prev[id] })); }; const okLabel = !ready ? t("auth.ok") : (props.pending === "full" ? t("auth.ok.full") : t("auth.ok.tab")); return h("div", { className: "dshp-auth-mask", role: "dialog", "aria-modal": "true" }, h("div", { className: "dshp-auth-modal" }, h("h2", null, t("auth.title")), h("div", { className: "muted", style: { color: "var(--dsw-alias-label-secondary,#999)", fontSize: 12 } }, t("auth.lead")), h("div", { className: "dshp-auth-warn" }, t("auth.warn")), h("div", { className: "dshp-auth-legal", ref: legalRef, onScroll, dangerouslySetInnerHTML: { __html: AUTH_LEGAL_HTML }, }), h("div", { style: { color: "var(--dsw-alias-label-secondary,#999)", fontSize: 12 } }, hint), h("div", { className: "dshp-auth-checks" }, [["read", "auth.check.read"], ["scope", "auth.check.scope"], ["ban", "auth.check.ban"]].map((row) => h("label", { key: row[0], className: locked ? "locked" : "" }, h("input", { type: "checkbox", checked: !!checks[row[0]], disabled: locked, onChange: () => toggle(row[0]), }), h("span", null, t(row[1])), ), ), ), h("div", { className: "dshp-auth-ops" }, h("button", { type: "button", onClick: () => setDock({ authOpen: false, tab: "clean", pending: "tab" }) }, t("auth.no")), h("button", { type: "button", className: "primary", disabled: !ready, onClick: () => { if (!ready) return; writeDrillAuth(true); const kind = props.pending || "tab"; setDock({ authOpen: false, open: true, tab: "drill", pending: "tab" }); try { if (__dshPurgeDrill && __dshPurgeDrill.setUI) __dshPurgeDrill.setUI({ open: true }); } catch { /* ignore */ } if (kind === "full") { try { window.open(window.location.href.split("#")[0] + "#redteam-full", "_blank", "noopener"); } catch { /* ignore */ } } }, }, okLabel), ), ), ); } function PurgeDock() { const t = useT(); const st = useDock(); useEffect(() => { if (!__dshPurgeDrill || !__dshPurgeDrill.CSS) return undefined; const styleTag = document.createElement("style"); styleTag.setAttribute("data-dsh-purge-drill", "1"); styleTag.textContent = __dshPurgeDrill.CSS; document.head.append(styleTag); const widthTag = document.createElement("style"); widthTag.setAttribute("data-dsh-purge-dock-w", "1"); widthTag.textContent = ":root{--dshp-dock-w:620px;--rt-dock-w:620px}"; document.head.append(widthTag); try { if (__dshPurgeDrill.setDockWidth) __dshPurgeDrill.setDockWidth(620); } catch { /* ignore */ } return () => { styleTag.remove(); widthTag.remove(); }; }, []); useEffect(() => { if (st.tab === "drill" && readDrillAuth() && __dshPurgeDrill && __dshPurgeDrill.setUI) { try { __dshPurgeDrill.setUI({ open: true }); } catch { /* ignore */ } } }, [st.tab, st.open]); const onTab = (id) => { if (id === "drill") { requestDrill("tab"); return; } setDock({ tab: "clean", authOpen: false, pending: "tab" }); }; const body = st.tab === "clean" ? h(SettingsRoot, { t }) : (readDrillAuth() && __dshPurgeDrill.Panel ? h(__dshPurgeDrill.Panel, { embedded: true }) : h("div", { style: { padding: 16, color: "var(--dsw-alias-label-secondary)" } }, t("dock.unauthorized"))); return h(react.Fragment, null, h("div", { className: "dshp-dock", "data-open": st.open ? "1" : "0" }, h("div", { className: "dshp-dock-head" }, h("b", null, "dsh-purge"), h("span", { style: { flex: 1 } }), st.tab === "drill" && readDrillAuth() ? h("button", { type: "button", onClick: () => requestDrill("full") }, t("dock.full")) : null, h("button", { type: "button", onClick: () => setDock({ open: false, authOpen: false, tab: "clean", pending: "tab" }) }, t("dock.close")), ), h("div", { className: "dshp-dock-tabs", role: "tablist" }, h("button", { type: "button", className: "dshp-dock-tab" + (st.tab === "clean" ? " on" : ""), role: "tab", "aria-selected": st.tab === "clean" ? "true" : "false", onClick: () => onTab("clean"), }, t("dock.clean")), h("button", { type: "button", className: "dshp-dock-tab" + (st.tab === "drill" ? " on" : ""), role: "tab", "aria-selected": st.tab === "drill" ? "true" : "false", onClick: () => onTab("drill"), }, t("dock.drill") + (readDrillAuth() ? "" : " · " + t("dock.unauthorized"))), ), h("div", { className: "dshp-dock-body" }, body), ), st.authOpen ? h(AuthGate, { pending: st.pending }) : null, ); } function HeroNewSessionMount() { const t = useT(); const st = useDock(); const [host, setHost] = useState(null); useEffect(() => { let dead = false; const ensure = () => { if (dead || typeof document === "undefined") return; const row = document.querySelector('[class*="heroWorkspaceRow"]'); if (!row) { setHost((prev) => (prev ? null : prev)); return; } let el = row.querySelector(":scope > .dshp-hero-chip"); if (!el) { el = document.createElement("div"); el.className = "dshp-hero-chip"; row.appendChild(el); } setHost((prev) => (prev === el ? prev : el)); }; ensure(); const obs = typeof MutationObserver !== "undefined" ? new MutationObserver(() => ensure()) : null; if (obs) obs.observe(document.body, { childList: true, subtree: true }); const iv = setInterval(ensure, 1000); return () => { dead = true; if (obs) obs.disconnect(); clearInterval(iv); }; }, []); if (!host) return null; const btn = h("button", { type: "button", className: "dshp-hero-chip-btn" + (st.open ? " on" : ""), title: t("dock.newSession"), onClick: () => toggleDock(), }, st.open ? h("span", { className: "dshp-live-dot", "aria-hidden": "true" }) : null, t("dock.newSession"), ); try { const rd = require("react-dom"); if (rd && typeof rd.createPortal === "function") return rd.createPortal(btn, host); } catch { /* ignore */ } return null; } `; if (!client.includes("function PurgeDock()")) { const anchor = "\t\tfunction installRewindUi(ctx) {"; if (!client.includes(anchor)) throw new Error("installRewindUi not found"); client = client.replace(anchor, dockBlock + "\t\tfunction installRewindUi(ctx) {"); } // 4) Replace apply() settings.section with dock slots const oldApply = ` function apply(ctx) { ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-purge: dictionaries"); const t = ctx.locale.bind(NS); translate = t; ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "dsh-purge", order: 40, label: () => t("nav"), locale: NS, inject: () => ({ t }), }, SettingsRoot)); try { if (typeof ctx.inject === "function") { ctx.inject(["sessions", "uiWorkspace", "conversation"], (host) => installRewindUi(host)); } else if (ctx.sessions) { installRewindUi(ctx); } } catch (e) { try { console.warn("[dsh-purge] rewind ui skipped:", e); } catch { /* ignore */ } } }`; const newApply = ` function apply(ctx) { ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-purge: dictionaries"); const t = ctx.locale.bind(NS); translate = t; ctx.slots.inject("shell.overlay", () => ctx.slots.register({ name: "shell.overlay", id: "dsh-purge-dock", order: 50, }, () => h(react.Fragment, null, h(PurgeDock), h(HeroNewSessionMount), h(TabRowPurgeMount), h(EnvAdaptSendGate)))); try { if (typeof ctx.inject === "function") { ctx.inject(["sessions", "uiWorkspace", "workspaces", "conversation"], (host) => installRewindUi(host)); } else if (ctx.sessions) { installRewindUi(ctx); } } catch (e) { try { if (typeof ctx.inject === "function") { ctx.inject(["sessions", "uiWorkspace", "conversation"], (host) => installRewindUi(host)); } } catch (e2) { try { console.warn("[dsh-purge] rewind ui skipped:", e2 || e); } catch { /* ignore */ } } } }`; if (client.includes('id: "dsh-purge-dock"')) { // already patched apply } else if (client.includes(oldApply)) { client = client.replace(oldApply, newApply); } else { throw new Error("apply() block not found for replacement"); } writeFileSync(clientPath, client); console.log("patched dock/auth into client.js"); const built = spawnSync(process.execPath, [join(root, "scripts", "build-client.mjs")], { cwd: root, encoding: "utf8", }); process.stdout.write(built.stdout || ""); process.stderr.write(built.stderr || ""); if (built.status !== 0) process.exit(built.status || 1); console.log("done");