From 71a8ba12ba6adb223551bf27a4a334d7ed2c9ce3 Mon Sep 17 00:00:00 2001 From: Theros Date: Sun, 1 Mar 2026 12:19:34 +0000 Subject: [PATCH 1/8] Add quarantine clean mode and UI selection --- logic.ts | 58 +++++++++++++++++++++++--- logic_test.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++++++-- main.ts | 105 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 255 insertions(+), 18 deletions(-) diff --git a/logic.ts b/logic.ts index 0739f23..587c0eb 100644 --- a/logic.ts +++ b/logic.ts @@ -48,6 +48,14 @@ export type RenameExecutionResult = { export type CleanResult = { removedFiles: string[]; renameResults: RenameExecutionResult[]; + mode: CleanMode; + quarantineRunId?: string; +}; + +export type CleanMode = "delete" | "quarantine"; + +export type CleanFolderDetailedOptions = { + mode?: CleanMode; }; type KeeplistConfig = { @@ -319,7 +327,6 @@ async function pruneEmptyParentDirs( } } - function compileRule(rule: string): RegExp | null { const normalizedRule = normalizeRule(rule); if (!normalizedRule) { @@ -432,7 +439,11 @@ async function applyRenames( await Deno.stat(sourcePath); } catch (sourceError) { if (sourceError instanceof Deno.errors.NotFound) { - results.push({ ...rename, applied: false, reason: "missing_source" }); + results.push({ + ...rename, + applied: false, + reason: "missing_source", + }); continue; } @@ -446,11 +457,46 @@ async function applyRenames( return results; } -export async function cleanFolderDetailed(root: string): Promise { +function buildQuarantineRunId(): string { + return new Date().toISOString().replaceAll(":", "-"); +} + +async function quarantineFiles( + root: string, + removableFiles: string[], +): Promise { + const runId = buildQuarantineRunId(); + const quarantineRoot = join(root, ".modcleaner_quarantine", runId); + + for (const file of removableFiles) { + const sourcePath = join(root, file); + const targetPath = join(quarantineRoot, file); + await Deno.mkdir(dirname(targetPath), { recursive: true }); + await Deno.rename(sourcePath, targetPath); + } + + return runId; +} + +export async function cleanFolderDetailed( + root: string, + options: CleanFolderDetailedOptions = {}, +): Promise { const plan = await buildScanPlan(root); + const mode = options.mode ?? "delete"; - for (const file of plan.removableFiles) { - await Deno.remove(join(root, file)); + if (mode !== "delete" && mode !== "quarantine") { + throw new Error(`Invalid clean mode: ${mode}`); + } + + let quarantineRunId: string | undefined; + + if (mode === "delete") { + for (const file of plan.removableFiles) { + await Deno.remove(join(root, file)); + } + } else { + quarantineRunId = await quarantineFiles(root, plan.removableFiles); } const renameResults = await applyRenames(root, plan.plannedRenames); @@ -460,6 +506,8 @@ export async function cleanFolderDetailed(root: string): Promise { return { removedFiles: plan.removableFiles, renameResults, + mode, + quarantineRunId, }; } diff --git a/logic_test.ts b/logic_test.ts index e733789..2fbd5f0 100644 --- a/logic_test.ts +++ b/logic_test.ts @@ -353,6 +353,8 @@ Deno.test("cleanFolder applies !rename after deletion and reports rename results ); const result = await cleanFolderDetailed(root); + assertEquals(result.mode, "delete"); + assertEquals(result.quarantineRunId, undefined); assertEquals(result.removedFiles, [ "Game/Binaries/Win64/modloader-temp.dll", ]); @@ -387,6 +389,95 @@ Deno.test("cleanFolder applies !rename after deletion and reports rename results } }); +Deno.test("cleanFolderDetailed quarantine mode moves files into deterministic run layout without data loss", async () => { + const root = await Deno.makeTempDir(); + + const toIsoStub = stub( + Date.prototype, + "toISOString", + () => "2026-01-02T03:04:05.678Z", + ); + + try { + await Deno.mkdir(join(root, "keep"), { recursive: true }); + await Deno.mkdir(join(root, "trash", "nested"), { recursive: true }); + + await Deno.writeTextFile(join(root, "keep", "stay.txt"), "keep"); + await Deno.writeTextFile( + join(root, "trash", "nested", "remove.tmp"), + "remove-content", + ); + + await Deno.writeTextFile(join(root, KEEPLIST_FILE), "keep/**\n"); + + const result = await cleanFolderDetailed(root, { mode: "quarantine" }); + assertEquals(result.mode, "quarantine"); + assertEquals(result.quarantineRunId, "2026-01-02T03-04-05.678Z"); + assertEquals(result.removedFiles, ["trash/nested/remove.tmp"]); + + const sourceExists = await Deno.stat( + join(root, "trash", "nested", "remove.tmp"), + ) + .then(() => true) + .catch(() => false); + assertEquals(sourceExists, false); + + const quarantinedPath = join( + root, + ".modcleaner_quarantine", + "2026-01-02T03-04-05.678Z", + "trash", + "nested", + "remove.tmp", + ); + const quarantinedContent = await Deno.readTextFile(quarantinedPath); + assertEquals(quarantinedContent, "remove-content"); + } finally { + toIsoStub.restore(); + await Deno.remove(root, { recursive: true }); + } +}); + +Deno.test("cleanFolderDetailed quarantine mode still applies renames after removal phase", async () => { + const root = await Deno.makeTempDir(); + + try { + await Deno.mkdir(join(root, "keep"), { recursive: true }); + await Deno.mkdir(join(root, "trash"), { recursive: true }); + await Deno.mkdir(join(root, "restore"), { recursive: true }); + + await Deno.writeTextFile(join(root, "keep", "stay.txt"), "keep"); + await Deno.writeTextFile(join(root, "trash", "remove.tmp"), "remove"); + await Deno.writeTextFile(join(root, "restore", "backup.bin"), "backup"); + + await Deno.writeTextFile( + join(root, KEEPLIST_FILE), + [ + "keep/**", + "!rename restore/backup.bin -> restore/live.bin", + ].join("\n"), + ); + + const result = await cleanFolderDetailed(root, { mode: "quarantine" }); + assertEquals(result.renameResults, [ + { + from: "restore/backup.bin", + to: "restore/live.bin", + applied: true, + }, + ]); + + const renamedTargetExists = await Deno.stat( + join(root, "restore", "live.bin"), + ) + .then(() => true) + .catch(() => false); + assertEquals(renamedTargetExists, true); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("cleanFolderDetailed prunes empty parent directories of removed files", async () => { const root = await Deno.makeTempDir(); @@ -395,7 +486,10 @@ Deno.test("cleanFolderDetailed prunes empty parent directories of removed files" await Deno.mkdir(join(root, "trash", "nested"), { recursive: true }); await Deno.writeTextFile(join(root, "keep", "stay.txt"), "keep"); - await Deno.writeTextFile(join(root, "trash", "nested", "remove.tmp"), "remove"); + await Deno.writeTextFile( + join(root, "trash", "nested", "remove.tmp"), + "remove", + ); await Deno.writeTextFile(join(root, KEEPLIST_FILE), "keep/**\n"); @@ -699,7 +793,9 @@ Deno.test("cleanFolderDetailed treats missing rename target parent as fatal", as await assertRejects(() => cleanFolderDetailed(root), Error); - const removedStillMissing = await Deno.stat(join(root, "misc", "remove.tmp")) + const removedStillMissing = await Deno.stat( + join(root, "misc", "remove.tmp"), + ) .then(() => false) .catch(() => true); const sourceStillExists = await Deno.stat(join(root, "backup.bin")) @@ -734,7 +830,9 @@ Deno.test("cleanFolderDetailed propagates fatal rename errors without rollback", await assertRejects(() => cleanFolderDetailed(root), Error); - const removedStillMissing = await Deno.stat(join(root, "misc", "remove.tmp")) + const removedStillMissing = await Deno.stat( + join(root, "misc", "remove.tmp"), + ) .then(() => false) .catch(() => true); const sourceStillExists = await Deno.stat(join(root, "backup.bin")) @@ -773,7 +871,11 @@ Deno.test("cleanFolderDetailed propagates fatal target stat errors", async () => }); try { - await assertRejects(() => cleanFolderDetailed(root), Error, "synthetic stat failure"); + await assertRejects( + () => cleanFolderDetailed(root), + Error, + "synthetic stat failure", + ); } finally { statStub.restore(); } diff --git a/main.ts b/main.ts index 4b18ba6..83f2f15 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,11 @@ import { WebUI } from "WebUI"; import { FileDialog, load as loadNativeDialog } from "@miyauci/rfd/deno"; -import { buildScanPlan, cleanFolderDetailed, writeKeeplist } from "./logic.ts"; +import { + buildScanPlan, + cleanFolderDetailed, + type CleanMode, + writeKeeplist, +} from "./logic.ts"; const html = ` @@ -95,6 +100,22 @@ input { color: var(--text); font-size: 14px; } +select { + flex: 1; + min-width: 0; + height: 40px; + padding: 0 12px; + border-radius: 8px; + border: 1px solid var(--border); + background: #181b21; + color: var(--text); + font-size: 14px; +} +select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25); +} input:focus { outline: none; border-color: var(--accent); @@ -140,6 +161,17 @@ button.danger:hover:not(:disabled) { display: flex; gap: 10px; } +.mode-row { + margin-top: 12px; + display: flex; + gap: 10px; + align-items: center; +} +.mode-help { + margin-top: 8px; + font-size: 13px; + color: var(--muted); +} .actions .danger-wrap { margin-left: auto; padding-left: 10px; @@ -308,6 +340,15 @@ button.danger:hover:not(:disabled) { +
+ + +
+
+
Idle
@@ -331,7 +372,7 @@ button.danger:hover:not(:disabled) {
Scan checks the folder against #keeplist.txt and lists files that would be removed.
Scan and Clean require #keeplist.txt to exist and contain at least one rule.
-
Clean permanently deletes files not matching any keep rule. Use with caution.
+
Clean can permanently delete files or move them into .modcleaner_quarantine/<timestamp>.
@@ -374,6 +415,26 @@ function setBusy(isBusy) { } }); } + +function getSelectedCleanMode() { + const mode = document.getElementById("cleanMode")?.value; + return mode === "quarantine" ? "quarantine" : "delete"; +} + +function updateCleanModeHelp() { + const mode = getSelectedCleanMode(); + const help = document.getElementById("cleanModeHelp"); + if (!help) { + return; + } + + if (mode === "delete") { + help.innerText = "Warning: Delete mode permanently removes files after confirmation."; + return; + } + + help.innerText = "Quarantine mode moves removable files to .modcleaner_quarantine// under the selected game folder."; +} function setCleanReady(isReady) { cleanReady = Boolean(isReady); setBusy(busy); @@ -467,6 +528,7 @@ document.addEventListener("keydown", (event) => { setResults({ removals: [], renames: [] }); setCleanReady(false); +updateCleanModeHelp(); @@ -477,7 +539,9 @@ function runStatus( message: string, type: "info" | "success" | "error" = "info", ): void { - event.window.run(`setStatus(${JSON.stringify(message)}, ${JSON.stringify(type)})`); + event.window.run( + `setStatus(${JSON.stringify(message)}, ${JSON.stringify(type)})`, + ); } function runBusy(event: WebUI.Event, isBusy: boolean): void { @@ -489,10 +553,15 @@ function runCleanReady(event: WebUI.Event, isReady: boolean): void { } async function showCleanupConfirmation(event: WebUI.Event): Promise { + const mode = await getCleanMode(event); + const message = mode === "delete" + ? "Delete mode permanently removes files that are not covered by #keeplist.txt. Continue?" + : "Quarantine mode moves removable files to .modcleaner_quarantine// inside the selected game folder. Continue?"; + const confirmedValue: unknown = await event.window.script(` return showConfirmDialog( "Confirm Cleanup", - "This will permanently delete files that are not covered by #keeplist.txt. Continue?", + ${JSON.stringify(message)}, "Clean" ); `); @@ -501,6 +570,14 @@ async function showCleanupConfirmation(event: WebUI.Event): Promise { (typeof confirmedValue === "string" && confirmedValue === "true"); } +async function getCleanMode(event: WebUI.Event): Promise { + const value = await event.window.script( + "return (document.getElementById('cleanMode')?.value || 'delete');", + ); + + return value === "quarantine" ? "quarantine" : "delete"; +} + function runResults( event: WebUI.Event, removals: string[], @@ -566,7 +643,11 @@ async function generateKeeplist(event: WebUI.Event): Promise { const files = await writeKeeplist(root); lastScannedRoot = null; runCleanReady(event, false); - runStatus(event, `#keeplist.txt generated (${files.length} entries)`, "success"); + runStatus( + event, + `#keeplist.txt generated (${files.length} entries)`, + "success", + ); } catch (error) { runStatus(event, `Failed to generate keeplist: ${String(error)}`, "error"); } finally { @@ -624,10 +705,14 @@ async function cleanFiles(event: WebUI.Event): Promise { return; } + const mode = await getCleanMode(event); + try { runBusy(event, true); - const result = await cleanFolderDetailed(root); - const appliedRenames = result.renameResults.filter((rename) => rename.applied) + const result = await cleanFolderDetailed(root, { mode }); + const appliedRenames = result.renameResults.filter((rename) => + rename.applied + ) .length; const missingSourceRenames = result.renameResults.filter((rename) => rename.reason === "missing_source" @@ -643,9 +728,12 @@ async function cleanFiles(event: WebUI.Event): Promise { to: rename.to, })), ); + const modeSummary = result.mode === "quarantine" && result.quarantineRunId + ? `quarantined at .modcleaner_quarantine/${result.quarantineRunId}` + : "deleted permanently"; runStatus( event, - `Cleanup complete (${result.removedFiles.length} file(s) removed, renames: ${appliedRenames} applied, ${missingSourceRenames} missing source, ${targetExistsRenames} target exists)`, + `Cleanup complete (${result.removedFiles.length} file(s) ${modeSummary}, renames: ${appliedRenames} applied, ${missingSourceRenames} missing source, ${targetExistsRenames} target exists)`, "success", ); } catch (error) { @@ -668,4 +756,3 @@ win.bind("cleanFiles", cleanFiles); await win.showBrowser(html, WebUI.Browser.AnyBrowser); await WebUI.wait(); - From 50be33e989e59c3b87119d768632fac3c240b3c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:25:33 +0000 Subject: [PATCH 2/8] Initial plan From 378b01dbdddb9ded7b91d728d714833408aa9bc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:31:05 +0000 Subject: [PATCH 3/8] Fix quarantine dir exclusion from walk and empty-files runId Co-authored-by: Therosin <747653+Therosin@users.noreply.github.com> --- deno.lock | 1768 ++++--------------------------------------------- logic.ts | 13 +- logic_test.ts | 47 ++ 3 files changed, 170 insertions(+), 1658 deletions(-) diff --git a/deno.lock b/deno.lock index 8600e9b..0322651 100644 --- a/deno.lock +++ b/deno.lock @@ -1,1660 +1,116 @@ { - "version": "5", - "specifiers": { - "jsr:@denosaurs/plug@^1.0.6": "1.1.0", - "jsr:@miyauci/rfd@*": "1.0.0", - "jsr:@miyauci/rfd@1": "1.0.0", - "jsr:@std/assert@1": "1.0.18", - "jsr:@std/assert@^1.0.17": "1.0.18", - "jsr:@std/encoding@1": "1.0.10", - "jsr:@std/fmt@1": "1.0.8", - "jsr:@std/fs@1": "1.0.23", - "jsr:@std/fs@^1.0.23": "1.0.23", - "jsr:@std/internal@^1.0.12": "1.0.12", - "jsr:@std/path@*": "1.1.4", - "jsr:@std/path@1": "1.1.4", - "jsr:@std/path@^1.1.4": "1.1.4", - "jsr:@std/testing@1": "1.0.17", - "jsr:@zip-js/zip-js@^2.8.21": "2.8.21", - "npm:docfork@*": "1.3.4_zod@4.3.5" - }, - "jsr": { - "@denosaurs/plug@1.1.0": { - "integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044", - "dependencies": [ - "jsr:@std/encoding", - "jsr:@std/fmt", - "jsr:@std/fs@1", - "jsr:@std/path@1" - ] - }, - "@miyauci/rfd@1.0.0": { - "integrity": "be8b7b21fc79fd657cac6b7f7a94ab9ffe40ea0c63c1984d4d4360cd1759923a", - "dependencies": [ - "jsr:@denosaurs/plug" - ] - }, - "@std/assert@1.0.18": { - "integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/encoding@1.0.10": { - "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" - }, - "@std/fmt@1.0.8": { - "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" - }, - "@std/fs@1.0.23": { - "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", - "dependencies": [ - "jsr:@std/internal", - "jsr:@std/path@^1.1.4" - ] - }, - "@std/internal@1.0.12": { - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" - }, - "@std/path@1.1.4": { - "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/testing@1.0.17": { - "integrity": "87bdc2700fa98249d48a17cd72413352d3d3680dcfbdb64947fd0982d6bbf681", - "dependencies": [ - "jsr:@std/assert@^1.0.17" - ] - }, - "@zip-js/zip-js@2.8.21": { - "integrity": "0787af769e7ed64f0728beba0dd7b52a57d26f202c08c54f0200a48d1c38845d" - } - }, - "npm": { - "@cspotcode/source-map-support@0.8.1": { - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dependencies": [ - "@jridgewell/trace-mapping" - ] - }, - "@floating-ui/core@1.7.3": { - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "dependencies": [ - "@floating-ui/utils" - ] - }, - "@floating-ui/dom@1.7.4": { - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "dependencies": [ - "@floating-ui/core", - "@floating-ui/utils" - ] - }, - "@floating-ui/react-dom@2.1.6_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "dependencies": [ - "@floating-ui/dom", - "react", - "react-dom" - ] - }, - "@floating-ui/utils@0.2.10": { - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" - }, - "@hono/node-server@1.19.9_hono@4.11.4": { - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "dependencies": [ - "hono" - ] - }, - "@jridgewell/resolve-uri@3.1.2": { - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" - }, - "@jridgewell/sourcemap-codec@1.5.5": { - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - }, - "@jridgewell/trace-mapping@0.3.9": { - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dependencies": [ - "@jridgewell/resolve-uri", - "@jridgewell/sourcemap-codec" - ] - }, - "@modelcontextprotocol/inspector-cli@0.18.0_zod@3.25.76": { - "integrity": "sha512-QMPjKx8zKmX17S1LF2gWuwbYglKexkdgB0HhKZFXzGrQ0MYoKUsIgokMyV48xr4LipaLS3b2v3ut3nV/jhWeSg==", - "dependencies": [ - "@modelcontextprotocol/sdk@1.25.2_zod@3.25.76_ajv@8.17.1_express@5.2.1", - "commander", - "spawn-rx" - ], - "bin": true - }, - "@modelcontextprotocol/inspector-client@0.18.0_zod@3.25.76_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-M6A5SN09tYCoTTGwMi5hdQpesX5eHYn3FCAHTWfv1pZ1Cy7h5GjB1LxL5WhbMhXWmFFJ6AnQVGAJj0P0XkVhUg==", - "dependencies": [ - "@modelcontextprotocol/sdk@1.25.2_zod@3.25.76_ajv@8.17.1_express@5.2.1", - "@radix-ui/react-checkbox", - "@radix-ui/react-dialog", - "@radix-ui/react-icons", - "@radix-ui/react-label", - "@radix-ui/react-popover", - "@radix-ui/react-select", - "@radix-ui/react-slot@1.2.4_react@18.3.1", - "@radix-ui/react-switch", - "@radix-ui/react-tabs", - "@radix-ui/react-toast", - "@radix-ui/react-tooltip", - "ajv@6.12.6", - "class-variance-authority", - "clsx", - "cmdk", - "lucide-react", - "pkce-challenge@4.1.0", - "prismjs", - "react", - "react-dom", - "react-simple-code-editor", - "serve-handler", - "tailwind-merge", - "zod@3.25.76" - ], - "bin": true - }, - "@modelcontextprotocol/inspector-server@0.18.0_zod@3.25.76": { - "integrity": "sha512-N7mDwUuj+gB8ZbZ52M4Oqh37qChS8kWJUkc4qL/MMsaQTVshXEOTcyiQ/mLKa17O5uODZQerAnQJWZbZYReBkg==", - "dependencies": [ - "@modelcontextprotocol/sdk@1.25.2_zod@3.25.76_ajv@8.17.1_express@5.2.1", - "cors", - "express", - "shell-quote", - "spawn-rx", - "ws", - "zod@3.25.76" - ], - "bin": true - }, - "@modelcontextprotocol/inspector@0.18.0_zod@3.25.76": { - "integrity": "sha512-aBrBDaI8MtvyS9j3TMRgTHZaOwbe/zh2rbIVplIBtxWifaSfvQX9DbnoI3xv9sZjgeFyF/3CwZdfEVTUx2RfBg==", - "dependencies": [ - "@modelcontextprotocol/inspector-cli", - "@modelcontextprotocol/inspector-client", - "@modelcontextprotocol/inspector-server", - "@modelcontextprotocol/sdk@1.25.2_zod@3.25.76_ajv@8.17.1_express@5.2.1", - "concurrently", - "node-fetch", - "open", - "shell-quote", - "spawn-rx", - "ts-node", - "zod@3.25.76" - ], - "bin": true - }, - "@modelcontextprotocol/sdk@1.25.2_zod@3.25.76_ajv@8.17.1_express@5.2.1": { - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", - "dependencies": [ - "@hono/node-server", - "ajv@8.17.1", - "ajv-formats", - "content-type", - "cors", - "cross-spawn", - "eventsource", - "eventsource-parser", - "express", - "express-rate-limit", - "jose", - "json-schema-typed", - "pkce-challenge@5.0.1", - "raw-body", - "zod@3.25.76", - "zod-to-json-schema@3.25.1_zod@3.25.76" - ] - }, - "@modelcontextprotocol/sdk@1.25.2_zod@4.3.5_ajv@8.17.1_express@5.2.1": { - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", - "dependencies": [ - "@hono/node-server", - "ajv@8.17.1", - "ajv-formats", - "content-type", - "cors", - "cross-spawn", - "eventsource", - "eventsource-parser", - "express", - "express-rate-limit", - "jose", - "json-schema-typed", - "pkce-challenge@5.0.1", - "raw-body", - "zod@4.3.5", - "zod-to-json-schema@3.25.1_zod@4.3.5" - ] - }, - "@radix-ui/number@1.1.1": { - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" - }, - "@radix-ui/primitive@1.1.3": { - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" - }, - "@radix-ui/react-arrow@1.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "dependencies": [ - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "react", - "react-dom" - ] - }, - "@radix-ui/react-checkbox@1.3.3_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-controllable-state", - "@radix-ui/react-use-previous", - "@radix-ui/react-use-size", - "react", - "react-dom" - ] - }, - "@radix-ui/react-collection@1.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-slot@1.2.3_react@18.3.1", - "react", - "react-dom" - ] - }, - "@radix-ui/react-compose-refs@1.1.2_react@18.3.1": { - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-context@1.1.2_react@18.3.1": { - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-dialog@1.1.15_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-dismissable-layer", - "@radix-ui/react-focus-guards", - "@radix-ui/react-focus-scope", - "@radix-ui/react-id", - "@radix-ui/react-portal", - "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-slot@1.2.3_react@18.3.1", - "@radix-ui/react-use-controllable-state", - "aria-hidden", - "react", - "react-dom", - "react-remove-scroll" - ] - }, - "@radix-ui/react-direction@1.1.1_react@18.3.1": { - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-dismissable-layer@1.1.11_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-compose-refs", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-callback-ref", - "@radix-ui/react-use-escape-keydown", - "react", - "react-dom" - ] - }, - "@radix-ui/react-focus-guards@1.1.3_react@18.3.1": { - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-focus-scope@1.1.7_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-callback-ref", - "react", - "react-dom" - ] - }, - "@radix-ui/react-icons@1.3.2_react@18.3.1": { - "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-id@1.1.1_react@18.3.1": { - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dependencies": [ - "@radix-ui/react-use-layout-effect", - "react" - ] - }, - "@radix-ui/react-label@2.1.8_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", - "dependencies": [ - "@radix-ui/react-primitive@2.1.4_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "react", - "react-dom" - ] - }, - "@radix-ui/react-popover@1.1.15_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-dismissable-layer", - "@radix-ui/react-focus-guards", - "@radix-ui/react-focus-scope", - "@radix-ui/react-id", - "@radix-ui/react-popper", - "@radix-ui/react-portal", - "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-slot@1.2.3_react@18.3.1", - "@radix-ui/react-use-controllable-state", - "aria-hidden", - "react", - "react-dom", - "react-remove-scroll" - ] - }, - "@radix-ui/react-popper@1.2.8_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "dependencies": [ - "@floating-ui/react-dom", - "@radix-ui/react-arrow", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-callback-ref", - "@radix-ui/react-use-layout-effect", - "@radix-ui/react-use-rect", - "@radix-ui/react-use-size", - "@radix-ui/rect", - "react", - "react-dom" - ] - }, - "@radix-ui/react-portal@1.1.9_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "dependencies": [ - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-layout-effect", - "react", - "react-dom" - ] - }, - "@radix-ui/react-presence@1.1.5_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "@radix-ui/react-use-layout-effect", - "react", - "react-dom" - ] - }, - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dependencies": [ - "@radix-ui/react-slot@1.2.3_react@18.3.1", - "react", - "react-dom" - ] - }, - "@radix-ui/react-primitive@2.1.4_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "dependencies": [ - "@radix-ui/react-slot@1.2.4_react@18.3.1", - "react", - "react-dom" - ] - }, - "@radix-ui/react-roving-focus@1.1.11_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-collection", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-direction", - "@radix-ui/react-id", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-callback-ref", - "@radix-ui/react-use-controllable-state", - "react", - "react-dom" - ] - }, - "@radix-ui/react-select@2.2.6_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "dependencies": [ - "@radix-ui/number", - "@radix-ui/primitive", - "@radix-ui/react-collection", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-direction", - "@radix-ui/react-dismissable-layer", - "@radix-ui/react-focus-guards", - "@radix-ui/react-focus-scope", - "@radix-ui/react-id", - "@radix-ui/react-popper", - "@radix-ui/react-portal", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-slot@1.2.3_react@18.3.1", - "@radix-ui/react-use-callback-ref", - "@radix-ui/react-use-controllable-state", - "@radix-ui/react-use-layout-effect", - "@radix-ui/react-use-previous", - "@radix-ui/react-visually-hidden", - "aria-hidden", - "react", - "react-dom", - "react-remove-scroll" - ] - }, - "@radix-ui/react-slot@1.2.3_react@18.3.1": { - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "react" - ] - }, - "@radix-ui/react-slot@1.2.4_react@18.3.1": { - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "react" - ] - }, - "@radix-ui/react-switch@1.2.6_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-controllable-state", - "@radix-ui/react-use-previous", - "@radix-ui/react-use-size", - "react", - "react-dom" - ] - }, - "@radix-ui/react-tabs@1.1.13_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-context", - "@radix-ui/react-direction", - "@radix-ui/react-id", - "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-roving-focus", - "@radix-ui/react-use-controllable-state", - "react", - "react-dom" - ] - }, - "@radix-ui/react-toast@1.2.15_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-collection", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-dismissable-layer", - "@radix-ui/react-portal", - "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-use-callback-ref", - "@radix-ui/react-use-controllable-state", - "@radix-ui/react-use-layout-effect", - "@radix-ui/react-visually-hidden", - "react", - "react-dom" - ] - }, - "@radix-ui/react-tooltip@1.2.8_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-dismissable-layer", - "@radix-ui/react-id", - "@radix-ui/react-popper", - "@radix-ui/react-portal", - "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "@radix-ui/react-slot@1.2.3_react@18.3.1", - "@radix-ui/react-use-controllable-state", - "@radix-ui/react-visually-hidden", - "react", - "react-dom" - ] - }, - "@radix-ui/react-use-callback-ref@1.1.1_react@18.3.1": { - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-use-controllable-state@1.2.2_react@18.3.1": { - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dependencies": [ - "@radix-ui/react-use-effect-event", - "@radix-ui/react-use-layout-effect", - "react" - ] - }, - "@radix-ui/react-use-effect-event@0.0.2_react@18.3.1": { - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "dependencies": [ - "@radix-ui/react-use-layout-effect", - "react" - ] - }, - "@radix-ui/react-use-escape-keydown@1.1.1_react@18.3.1": { - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "dependencies": [ - "@radix-ui/react-use-callback-ref", - "react" - ] - }, - "@radix-ui/react-use-layout-effect@1.1.1_react@18.3.1": { - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-use-previous@1.1.1_react@18.3.1": { - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "dependencies": [ - "react" - ] - }, - "@radix-ui/react-use-rect@1.1.1_react@18.3.1": { - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dependencies": [ - "@radix-ui/rect", - "react" - ] - }, - "@radix-ui/react-use-size@1.1.1_react@18.3.1": { - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dependencies": [ - "@radix-ui/react-use-layout-effect", - "react" - ] - }, - "@radix-ui/react-visually-hidden@1.2.3_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "dependencies": [ - "@radix-ui/react-primitive@2.1.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "react", - "react-dom" - ] - }, - "@radix-ui/rect@1.1.1": { - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" - }, - "@tsconfig/node10@1.0.12": { - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==" - }, - "@tsconfig/node12@1.0.11": { - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" - }, - "@tsconfig/node14@1.0.3": { - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" - }, - "@tsconfig/node16@1.0.4": { - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" - }, - "@types/node@25.2.3": { - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", - "dependencies": [ - "undici-types" - ] - }, - "accepts@2.0.0": { - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": [ - "mime-types@3.0.2", - "negotiator" - ] - }, - "acorn-walk@8.3.4": { - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dependencies": [ - "acorn" - ] - }, - "acorn@8.15.0": { - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "bin": true - }, - "ajv-formats@3.0.1_ajv@8.17.1": { - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dependencies": [ - "ajv@8.17.1" - ], - "optionalPeers": [ - "ajv@8.17.1" - ] - }, - "ajv@6.12.6": { - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": [ - "fast-deep-equal", - "fast-json-stable-stringify", - "json-schema-traverse@0.4.1", - "uri-js" - ] - }, - "ajv@8.17.1": { - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": [ - "fast-deep-equal", - "fast-uri", - "json-schema-traverse@1.0.0", - "require-from-string" - ] - }, - "ansi-regex@5.0.1": { - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles@4.3.0": { - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": [ - "color-convert" - ] - }, - "arg@4.1.3": { - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "aria-hidden@1.2.6": { - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "dependencies": [ - "tslib" - ] - }, - "balanced-match@1.0.2": { - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "body-parser@2.2.2": { - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dependencies": [ - "bytes@3.1.2", - "content-type", - "debug", - "http-errors", - "iconv-lite", - "on-finished", - "qs", - "raw-body", - "type-is" - ] - }, - "brace-expansion@1.1.12": { - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dependencies": [ - "balanced-match", - "concat-map" - ] - }, - "bundle-name@4.1.0": { - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dependencies": [ - "run-applescript" - ] - }, - "bytes@3.0.0": { - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" - }, - "bytes@3.1.2": { - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind-apply-helpers@1.0.2": { - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bound@1.0.4": { - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": [ - "call-bind-apply-helpers", - "get-intrinsic" - ] - }, - "chalk@4.1.2": { - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": [ - "ansi-styles", - "supports-color@7.2.0" - ] - }, - "class-variance-authority@0.7.1": { - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "dependencies": [ - "clsx" - ] - }, - "cliui@8.0.1": { - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": [ - "string-width", - "strip-ansi", - "wrap-ansi" - ] - }, - "clsx@2.1.1": { - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "cmdk@1.1.1_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "@radix-ui/react-dialog", - "@radix-ui/react-id", - "@radix-ui/react-primitive@2.1.4_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "react", - "react-dom" - ] - }, - "color-convert@2.0.1": { - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": [ - "color-name" - ] - }, - "color-name@1.1.4": { - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "commander@13.1.0": { - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==" - }, - "concat-map@0.0.1": { - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "concurrently@9.2.1": { - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dependencies": [ - "chalk", - "rxjs", - "shell-quote", - "supports-color@8.1.1", - "tree-kill", - "yargs" - ], - "bin": true - }, - "content-disposition@0.5.2": { - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==" - }, - "content-disposition@1.0.1": { - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==" - }, - "content-type@1.0.5": { - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "cookie-signature@1.2.2": { - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" - }, - "cookie@0.7.2": { - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" - }, - "cors@2.8.5": { - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": [ - "object-assign", - "vary" - ] - }, - "create-require@1.1.1": { - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "cross-spawn@7.0.6": { - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": [ - "path-key", - "shebang-command", - "which" - ] - }, - "data-uri-to-buffer@4.0.1": { - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" - }, - "debug@4.4.3": { - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": [ - "ms" - ] - }, - "default-browser-id@5.0.1": { - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==" - }, - "default-browser@5.5.0": { - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dependencies": [ - "bundle-name", - "default-browser-id" - ] - }, - "define-lazy-prop@3.0.0": { - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==" - }, - "depd@2.0.0": { - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "detect-node-es@1.1.0": { - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, - "diff@4.0.4": { - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==" - }, - "docfork@1.3.4_zod@4.3.5": { - "integrity": "sha512-d7C/J8U4nK/PK57rgGOXdm6nYcz2ybbXidgs8gETK+1WRb9dmUbhf8w24VBYjBhCS5Q8kCfX+K5LHcMakWuu+w==", - "dependencies": [ - "@modelcontextprotocol/inspector", - "@modelcontextprotocol/sdk@1.25.2_zod@4.3.5_ajv@8.17.1_express@5.2.1", - "dotenv", - "zod@4.3.5" - ], - "bin": true - }, - "dotenv@17.2.3": { - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==" - }, - "dunder-proto@1.0.1": { - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, - "ee-first@1.1.1": { - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "emoji-regex@8.0.0": { - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl@2.0.0": { - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms@1.1.1": { - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": [ - "es-errors" - ] - }, - "escalade@3.2.0": { - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" - }, - "escape-html@1.0.3": { - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag@1.8.1": { - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "eventsource-parser@3.0.6": { - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==" - }, - "eventsource@3.0.7": { - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dependencies": [ - "eventsource-parser" - ] - }, - "express-rate-limit@7.5.1_express@5.2.1": { - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "dependencies": [ - "express" - ] - }, - "express@5.2.1": { - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dependencies": [ - "accepts", - "body-parser", - "content-disposition@1.0.1", - "content-type", - "cookie", - "cookie-signature", - "debug", - "depd", - "encodeurl", - "escape-html", - "etag", - "finalhandler", - "fresh", - "http-errors", - "merge-descriptors", - "mime-types@3.0.2", - "on-finished", - "once", - "parseurl", - "proxy-addr", - "qs", - "range-parser@1.2.1", - "router", - "send", - "serve-static", - "statuses", - "type-is", - "vary" - ] - }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify@2.1.0": { - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-uri@3.1.0": { - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" - }, - "fetch-blob@3.2.0": { - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dependencies": [ - "node-domexception", - "web-streams-polyfill" - ] - }, - "finalhandler@2.1.1": { - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dependencies": [ - "debug", - "encodeurl", - "escape-html", - "on-finished", - "parseurl", - "statuses" - ] - }, - "formdata-polyfill@4.0.10": { - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": [ - "fetch-blob" - ] - }, - "forwarded@0.2.0": { - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh@2.0.0": { - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-caller-file@2.0.5": { - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic@1.3.0": { - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "get-proto", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] - }, - "get-nonce@1.0.1": { - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" - }, - "get-proto@1.0.1": { - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": [ - "dunder-proto", - "es-object-atoms" - ] - }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "has-flag@4.0.0": { - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": [ - "function-bind" - ] - }, - "hono@4.11.4": { - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==" - }, - "http-errors@2.0.1": { - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dependencies": [ - "depd", - "inherits", - "setprototypeof", - "statuses", - "toidentifier" - ] - }, - "iconv-lite@0.7.2": { - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dependencies": [ - "safer-buffer" - ] - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js@1.9.1": { - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-docker@3.0.0": { - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "bin": true - }, - "is-fullwidth-code-point@3.0.0": { - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-inside-container@1.0.0": { - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dependencies": [ - "is-docker" - ], - "bin": true - }, - "is-promise@4.0.0": { - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "is-wsl@3.1.0": { - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dependencies": [ - "is-inside-container" - ] - }, - "isexe@2.0.0": { - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "jose@6.1.3": { - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==" - }, - "js-tokens@4.0.0": { - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "json-schema-traverse@0.4.1": { - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-schema-traverse@1.0.0": { - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "json-schema-typed@8.0.2": { - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==" - }, - "loose-envify@1.4.0": { - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": [ - "js-tokens" - ], - "bin": true - }, - "lucide-react@0.523.0_react@18.3.1": { - "integrity": "sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw==", - "dependencies": [ - "react" - ] - }, - "make-error@1.3.6": { - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "math-intrinsics@1.1.0": { - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "media-typer@1.1.0": { - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" - }, - "merge-descriptors@2.0.0": { - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" - }, - "mime-db@1.33.0": { - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-db@1.54.0": { - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types@2.1.18": { - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dependencies": [ - "mime-db@1.33.0" - ] - }, - "mime-types@3.0.2": { - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": [ - "mime-db@1.54.0" - ] - }, - "minimatch@3.1.2": { - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": [ - "brace-expansion" - ] - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "negotiator@1.0.0": { - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" - }, - "node-domexception@1.0.0": { - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": true - }, - "node-fetch@3.3.2": { - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": [ - "data-uri-to-buffer", - "fetch-blob", - "formdata-polyfill" - ] - }, - "object-assign@4.1.1": { - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect@1.13.4": { - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" - }, - "on-finished@2.4.1": { - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": [ - "ee-first" - ] - }, - "once@1.4.0": { - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": [ - "wrappy" - ] - }, - "open@10.2.0": { - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dependencies": [ - "default-browser", - "define-lazy-prop", - "is-inside-container", - "wsl-utils" - ] - }, - "parseurl@1.3.3": { - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-is-inside@1.0.2": { - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" - }, - "path-key@3.1.1": { - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-to-regexp@3.3.0": { - "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" - }, - "path-to-regexp@8.3.0": { - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==" - }, - "pkce-challenge@4.1.0": { - "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==" - }, - "pkce-challenge@5.0.1": { - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==" - }, - "prismjs@1.30.0": { - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==" - }, - "proxy-addr@2.0.7": { - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": [ - "forwarded", - "ipaddr.js" - ] - }, - "punycode@2.3.1": { - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, - "qs@6.14.1": { - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "dependencies": [ - "side-channel" - ] - }, - "range-parser@1.2.0": { - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==" - }, - "range-parser@1.2.1": { - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body@3.0.2": { - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dependencies": [ - "bytes@3.1.2", - "http-errors", - "iconv-lite", - "unpipe" - ] - }, - "react-dom@18.3.1_react@18.3.1": { - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": [ - "loose-envify", - "react", - "scheduler" - ] - }, - "react-remove-scroll-bar@2.3.8_react@18.3.1": { - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "dependencies": [ - "react", - "react-style-singleton", - "tslib" - ] - }, - "react-remove-scroll@2.7.2_react@18.3.1": { - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "dependencies": [ - "react", - "react-remove-scroll-bar", - "react-style-singleton", - "tslib", - "use-callback-ref", - "use-sidecar" - ] - }, - "react-simple-code-editor@0.14.1_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", - "dependencies": [ - "react", - "react-dom" - ] - }, - "react-style-singleton@2.2.3_react@18.3.1": { - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "dependencies": [ - "get-nonce", - "react", - "tslib" - ] - }, - "react@18.3.1": { - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": [ - "loose-envify" - ] - }, - "require-directory@2.1.1": { - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-from-string@2.0.2": { - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "router@2.2.0": { - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": [ - "debug", - "depd", - "is-promise", - "parseurl", - "path-to-regexp@8.3.0" - ] - }, - "run-applescript@7.1.0": { - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==" - }, - "rxjs@7.8.2": { - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dependencies": [ - "tslib" - ] - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "scheduler@0.23.2": { - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": [ - "loose-envify" - ] - }, - "send@1.2.1": { - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dependencies": [ - "debug", - "encodeurl", - "escape-html", - "etag", - "fresh", - "http-errors", - "mime-types@3.0.2", - "ms", - "on-finished", - "range-parser@1.2.1", - "statuses" - ] - }, - "serve-handler@6.1.6": { - "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", - "dependencies": [ - "bytes@3.0.0", - "content-disposition@0.5.2", - "mime-types@2.1.18", - "minimatch", - "path-is-inside", - "path-to-regexp@3.3.0", - "range-parser@1.2.0" - ] - }, - "serve-static@2.2.1": { - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dependencies": [ - "encodeurl", - "escape-html", - "parseurl", - "send" - ] - }, - "setprototypeof@1.2.0": { - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "shebang-command@2.0.0": { - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": [ - "shebang-regex" - ] - }, - "shebang-regex@3.0.0": { - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "shell-quote@1.8.3": { - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==" - }, - "side-channel-list@1.0.0": { - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": [ - "es-errors", - "object-inspect" - ] - }, - "side-channel-map@1.0.1": { - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect" - ] - }, - "side-channel-weakmap@1.0.2": { - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect", - "side-channel-map" - ] - }, - "side-channel@1.1.0": { - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": [ - "es-errors", - "object-inspect", - "side-channel-list", - "side-channel-map", - "side-channel-weakmap" - ] - }, - "spawn-rx@5.1.2": { - "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==", - "dependencies": [ - "debug", - "rxjs" - ] - }, - "statuses@2.0.2": { - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" - }, - "string-width@4.2.3": { - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": [ - "emoji-regex", - "is-fullwidth-code-point", - "strip-ansi" - ] - }, - "strip-ansi@6.0.1": { - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": [ - "ansi-regex" - ] - }, - "supports-color@7.2.0": { - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": [ - "has-flag" - ] - }, - "supports-color@8.1.1": { - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": [ - "has-flag" - ] - }, - "tailwind-merge@2.6.0": { - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==" - }, - "toidentifier@1.0.1": { - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tree-kill@1.2.2": { - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "bin": true - }, - "ts-node@10.9.2_@types+node@25.2.3_typescript@5.9.3": { - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dependencies": [ - "@cspotcode/source-map-support", - "@tsconfig/node10", - "@tsconfig/node12", - "@tsconfig/node14", - "@tsconfig/node16", - "@types/node", - "acorn", - "acorn-walk", - "arg", - "create-require", - "diff", - "make-error", - "typescript", - "v8-compile-cache-lib", - "yn" - ], - "bin": true - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "type-is@2.0.1": { - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dependencies": [ - "content-type", - "media-typer", - "mime-types@3.0.2" - ] - }, - "typescript@5.9.3": { - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "bin": true - }, - "undici-types@7.16.0": { - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" - }, - "unpipe@1.0.0": { - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "uri-js@4.4.1": { - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": [ - "punycode" - ] - }, - "use-callback-ref@1.3.3_react@18.3.1": { - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "dependencies": [ - "react", - "tslib" - ] - }, - "use-sidecar@1.1.3_react@18.3.1": { - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "dependencies": [ - "detect-node-es", - "react", - "tslib" - ] - }, - "v8-compile-cache-lib@3.0.1": { - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" - }, - "vary@1.1.2": { - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "web-streams-polyfill@3.3.3": { - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" - }, - "which@2.0.2": { - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": [ - "isexe" - ], - "bin": true - }, - "wrap-ansi@7.0.0": { - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": [ - "ansi-styles", - "string-width", - "strip-ansi" - ] - }, - "wrappy@1.0.2": { - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "ws@8.19.0": { - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==" - }, - "wsl-utils@0.1.0": { - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dependencies": [ - "is-wsl" - ] - }, - "y18n@5.0.8": { - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yargs-parser@21.1.1": { - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - }, - "yargs@17.7.2": { - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": [ - "cliui", - "escalade", - "get-caller-file", - "require-directory", - "string-width", - "y18n", - "yargs-parser" - ] - }, - "yn@3.1.1": { - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" - }, - "zod-to-json-schema@3.25.1_zod@3.25.76": { - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dependencies": [ - "zod@3.25.76" - ] - }, - "zod-to-json-schema@3.25.1_zod@4.3.5": { - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dependencies": [ - "zod@4.3.5" - ] - }, - "zod@3.25.76": { - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" - }, - "zod@4.3.5": { - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==" - } - }, - "redirects": { - "https://deno.land/x/webui/mod.ts": "https://deno.land/x/webui@2.5.13/mod.ts" - }, + "version": "4", "remote": { - "https://deno.land/x/webui@2.5.13/deno.json": "533791ee2a3f2bd716b10b7ff1d0d4e7aa9c87c5f07944067c8b57117ca501d8", - "https://deno.land/x/webui@2.5.13/deps.ts": "c10de0be0b0e7541b4c281755480031ba6e95d995409fcd4f6eac2258aff1fa9", - "https://deno.land/x/webui@2.5.13/mod.ts": "c602b672baaeca93e07cb5fcae2013713d5ef127bddf87e65419f897f1e9fba3", - "https://deno.land/x/webui@2.5.13/src/lib.ts": "1db16bd855a3282b57ef0c11333ad4ddb353050802d6a1ad216b870c31d1fc09", - "https://deno.land/x/webui@2.5.13/src/types.ts": "969e8b75d6d9d6b23c4e26ce1cb4adcf34607df4c6d665f73a36091f1caecf49", - "https://deno.land/x/webui@2.5.13/src/utils.ts": "37d26ec3c0160da746f2367cd2ea4ebad1552965d0d0b3f5338a7876698e7486", - "https://deno.land/x/webui@2.5.13/src/webui.ts": "eb4e35890f5329ad6b9ab00ff405c88b2fec6a65acd0629aecde52eec9c2062d" - }, - "workspace": { - "dependencies": [ - "jsr:@miyauci/rfd@1", - "jsr:@std/assert@1", - "jsr:@std/fs@^1.0.23", - "jsr:@std/path@1", - "jsr:@std/testing@1", - "jsr:@zip-js/zip-js@^2.8.21" - ] + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", + "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", + "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", + "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", + "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", + "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", + "https://deno.land/std@0.224.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db" } } diff --git a/logic.ts b/logic.ts index 587c0eb..4babf0d 100644 --- a/logic.ts +++ b/logic.ts @@ -8,6 +8,7 @@ import { } from "@std/path"; export const KEEPLIST_FILE = "#keeplist.txt"; +export const QUARANTINE_DIR = ".modcleaner_quarantine"; export const GENERATED_KEEPLIST_HEADER = [ "# This file defines which files should be preserved during the Clean operation.", "# The !rename directive specifies a literal backup restore rule.", @@ -74,6 +75,10 @@ export async function walkFiles(root: string): Promise { continue; } if (entry.isDirectory) { + const relPath = relative(root, fullPath).replaceAll("\\", "/"); + if (relPath === QUARANTINE_DIR || relPath.startsWith(`${QUARANTINE_DIR}/`)) { + continue; + } await walk(fullPath); } } @@ -464,9 +469,13 @@ function buildQuarantineRunId(): string { async function quarantineFiles( root: string, removableFiles: string[], -): Promise { +): Promise { + if (removableFiles.length === 0) { + return undefined; + } + const runId = buildQuarantineRunId(); - const quarantineRoot = join(root, ".modcleaner_quarantine", runId); + const quarantineRoot = join(root, QUARANTINE_DIR, runId); for (const file of removableFiles) { const sourcePath = join(root, file); diff --git a/logic_test.ts b/logic_test.ts index 2fbd5f0..4015b6e 100644 --- a/logic_test.ts +++ b/logic_test.ts @@ -9,6 +9,7 @@ import { listRelativeFiles, matchesKeepRule, normalizeRelativePath, + QUARANTINE_DIR, readKeeplist, scanForRemoval, writeKeeplist, @@ -478,6 +479,52 @@ Deno.test("cleanFolderDetailed quarantine mode still applies renames after remov } }); +Deno.test("cleanFolderDetailed quarantine mode returns undefined runId when no files are removed", async () => { + const root = await Deno.makeTempDir(); + + try { + await Deno.mkdir(join(root, "keep"), { recursive: true }); + await Deno.writeTextFile(join(root, "keep", "stay.txt"), "keep"); + await Deno.writeTextFile(join(root, KEEPLIST_FILE), "keep/**\n"); + + const result = await cleanFolderDetailed(root, { mode: "quarantine" }); + assertEquals(result.mode, "quarantine"); + assertEquals(result.quarantineRunId, undefined); + assertEquals(result.removedFiles, []); + + const quarantineDirExists = await Deno.stat(join(root, QUARANTINE_DIR)) + .then(() => true) + .catch(() => false); + assertEquals(quarantineDirExists, false); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + +Deno.test("listRelativeFiles and buildScanPlan exclude quarantine directory", async () => { + const root = await Deno.makeTempDir(); + + try { + await Deno.mkdir(join(root, "keep"), { recursive: true }); + await Deno.mkdir(join(root, QUARANTINE_DIR, "2026-01-01T00-00-00.000Z", "trash"), { recursive: true }); + + await Deno.writeTextFile(join(root, "keep", "stay.txt"), "keep"); + await Deno.writeTextFile( + join(root, QUARANTINE_DIR, "2026-01-01T00-00-00.000Z", "trash", "old.tmp"), + "old", + ); + await Deno.writeTextFile(join(root, KEEPLIST_FILE), "keep/**\n"); + + const files = await listRelativeFiles(root); + assertEquals(files.includes(`${QUARANTINE_DIR}/2026-01-01T00-00-00.000Z/trash/old.tmp`), false); + + const plan = await buildScanPlan(root); + assertEquals(plan.removableFiles.some((f) => f.startsWith(QUARANTINE_DIR)), false); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("cleanFolderDetailed prunes empty parent directories of removed files", async () => { const root = await Deno.makeTempDir(); From 3b919ce121ba11bbceb448f52c900cc773641dec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:51:01 +0000 Subject: [PATCH 4/8] Use QUARANTINE_DIR constant consistently across all files; regenerate lockfile Co-authored-by: Therosin <747653+Therosin@users.noreply.github.com> --- deno.lock | 202 ++++++++++++++++++++++---------------------------- logic_test.ts | 2 +- main.ts | 9 ++- 3 files changed, 96 insertions(+), 117 deletions(-) diff --git a/deno.lock b/deno.lock index 0322651..bd0320c 100644 --- a/deno.lock +++ b/deno.lock @@ -1,116 +1,94 @@ { - "version": "4", + "version": "5", + "specifiers": { + "jsr:@denosaurs/plug@^1.0.6": "1.1.0", + "jsr:@miyauci/rfd@1": "1.0.0", + "jsr:@std/assert@1": "1.0.19", + "jsr:@std/assert@^1.0.17": "1.0.19", + "jsr:@std/encoding@1": "1.0.10", + "jsr:@std/fmt@1": "1.0.9", + "jsr:@std/fs@1": "1.0.23", + "jsr:@std/fs@^1.0.23": "1.0.23", + "jsr:@std/internal@^1.0.12": "1.0.12", + "jsr:@std/path@1": "1.1.4", + "jsr:@std/path@^1.1.4": "1.1.4", + "jsr:@std/testing@1": "1.0.17", + "jsr:@zip-js/zip-js@^2.8.21": "2.8.21" + }, + "jsr": { + "@denosaurs/plug@1.1.0": { + "integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044", + "dependencies": [ + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/fs@1", + "jsr:@std/path@1" + ] + }, + "@miyauci/rfd@1.0.0": { + "integrity": "be8b7b21fc79fd657cac6b7f7a94ab9ffe40ea0c63c1984d4d4360cd1759923a", + "dependencies": [ + "jsr:@denosaurs/plug" + ] + }, + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fmt@1.0.9": { + "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" + }, + "@std/fs@1.0.23": { + "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", + "dependencies": [ + "jsr:@std/internal", + "jsr:@std/path@^1.1.4" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + }, + "@std/path@1.1.4": { + "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/testing@1.0.17": { + "integrity": "87bdc2700fa98249d48a17cd72413352d3d3680dcfbdb64947fd0982d6bbf681", + "dependencies": [ + "jsr:@std/assert@^1.0.17" + ] + }, + "@zip-js/zip-js@2.8.21": { + "integrity": "0787af769e7ed64f0728beba0dd7b52a57d26f202c08c54f0200a48d1c38845d" + } + }, + "redirects": { + "https://deno.land/x/webui/mod.ts": "https://deno.land/x/webui@2.5.13/mod.ts" + }, "remote": { - "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", - "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", - "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", - "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", - "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", - "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", - "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", - "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", - "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", - "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", - "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", - "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", - "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", - "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", - "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", - "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", - "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", - "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", - "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", - "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", - "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", - "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", - "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", - "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", - "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", - "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", - "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", - "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", - "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", - "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", - "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", - "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", - "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", - "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", - "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", - "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", - "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", - "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", - "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", - "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", - "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", - "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", - "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", - "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", - "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", - "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", - "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", - "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", - "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", - "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", - "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", - "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", - "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", - "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", - "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", - "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", - "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", - "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", - "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", - "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", - "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", - "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", - "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", - "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", - "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", - "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", - "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", - "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", - "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", - "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", - "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", - "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", - "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", - "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", - "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", - "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", - "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", - "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", - "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", - "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", - "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", - "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", - "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", - "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", - "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", - "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", - "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", - "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", - "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", - "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", - "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", - "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", - "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", - "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", - "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", - "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", - "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", - "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", - "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", - "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", - "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", - "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", - "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", - "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", - "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", - "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", - "https://deno.land/std@0.224.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db" + "https://deno.land/x/webui@2.5.13/deno.json": "533791ee2a3f2bd716b10b7ff1d0d4e7aa9c87c5f07944067c8b57117ca501d8", + "https://deno.land/x/webui@2.5.13/deps.ts": "c10de0be0b0e7541b4c281755480031ba6e95d995409fcd4f6eac2258aff1fa9", + "https://deno.land/x/webui@2.5.13/mod.ts": "c602b672baaeca93e07cb5fcae2013713d5ef127bddf87e65419f897f1e9fba3", + "https://deno.land/x/webui@2.5.13/src/lib.ts": "1db16bd855a3282b57ef0c11333ad4ddb353050802d6a1ad216b870c31d1fc09", + "https://deno.land/x/webui@2.5.13/src/types.ts": "969e8b75d6d9d6b23c4e26ce1cb4adcf34607df4c6d665f73a36091f1caecf49", + "https://deno.land/x/webui@2.5.13/src/utils.ts": "37d26ec3c0160da746f2367cd2ea4ebad1552965d0d0b3f5338a7876698e7486", + "https://deno.land/x/webui@2.5.13/src/webui.ts": "eb4e35890f5329ad6b9ab00ff405c88b2fec6a65acd0629aecde52eec9c2062d" + }, + "workspace": { + "dependencies": [ + "jsr:@miyauci/rfd@1", + "jsr:@std/assert@1", + "jsr:@std/fs@^1.0.23", + "jsr:@std/path@1", + "jsr:@std/testing@1", + "jsr:@zip-js/zip-js@^2.8.21" + ] } } diff --git a/logic_test.ts b/logic_test.ts index 4015b6e..3431cb1 100644 --- a/logic_test.ts +++ b/logic_test.ts @@ -425,7 +425,7 @@ Deno.test("cleanFolderDetailed quarantine mode moves files into deterministic ru const quarantinedPath = join( root, - ".modcleaner_quarantine", + QUARANTINE_DIR, "2026-01-02T03-04-05.678Z", "trash", "nested", diff --git a/main.ts b/main.ts index 83f2f15..69f3f87 100644 --- a/main.ts +++ b/main.ts @@ -4,6 +4,7 @@ import { buildScanPlan, cleanFolderDetailed, type CleanMode, + QUARANTINE_DIR, writeKeeplist, } from "./logic.ts"; @@ -372,7 +373,7 @@ button.danger:hover:not(:disabled) {
Scan checks the folder against #keeplist.txt and lists files that would be removed.
Scan and Clean require #keeplist.txt to exist and contain at least one rule.
-
Clean can permanently delete files or move them into .modcleaner_quarantine/<timestamp>.
+
Clean can permanently delete files or move them into ${QUARANTINE_DIR}/<timestamp>.
@@ -433,7 +434,7 @@ function updateCleanModeHelp() { return; } - help.innerText = "Quarantine mode moves removable files to .modcleaner_quarantine// under the selected game folder."; + help.innerText = "Quarantine mode moves removable files to ${QUARANTINE_DIR}// under the selected game folder."; } function setCleanReady(isReady) { cleanReady = Boolean(isReady); @@ -556,7 +557,7 @@ async function showCleanupConfirmation(event: WebUI.Event): Promise { const mode = await getCleanMode(event); const message = mode === "delete" ? "Delete mode permanently removes files that are not covered by #keeplist.txt. Continue?" - : "Quarantine mode moves removable files to .modcleaner_quarantine// inside the selected game folder. Continue?"; + : `Quarantine mode moves removable files to ${QUARANTINE_DIR}// inside the selected game folder. Continue?`; const confirmedValue: unknown = await event.window.script(` return showConfirmDialog( @@ -729,7 +730,7 @@ async function cleanFiles(event: WebUI.Event): Promise { })), ); const modeSummary = result.mode === "quarantine" && result.quarantineRunId - ? `quarantined at .modcleaner_quarantine/${result.quarantineRunId}` + ? `quarantined at ${QUARANTINE_DIR}/${result.quarantineRunId}` : "deleted permanently"; runStatus( event, From d8d4d605e4e09dc7748d82e5144b691ae75c2b61 Mon Sep 17 00:00:00 2001 From: Theros Date: Sun, 1 Mar 2026 13:41:00 +0000 Subject: [PATCH 5/8] feat: enhance keeplist functionality with prefix support and update README --- README.md | 75 ++++++++++++---- logic.ts | 82 ++++++++++++++---- logic_test.ts | 136 ++++++++++++++++++++++++++++- main.ts | 236 ++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 470 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 6d0c520..feec446 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ModCleaner is a small desktop utility for cleaning a game folder with a simple keep-list. -It works by comparing the current contents of a game directory against a `#keeplist.txt` file. Anything not covered by the keeplist is treated as removable. This makes it useful for returning a heavily modded game install to a known-good state without manually sorting through every file. +It works by comparing the current contents of a game directory against the active keeplist file. By default that file is `#keeplist.txt`. If a keeplist prefix is active, the file becomes `#-keeplist.txt`. Anything not covered by the active keeplist is treated as removable. This makes it useful for returning a heavily modded game install to a known-good state without manually sorting through every file. ## What It Does @@ -18,13 +18,16 @@ ModCleaner provides three main actions: ### Generate Keeplist -Creates or overwrites `#keeplist.txt` inside the selected game folder. +Creates or overwrites the active keeplist inside the selected game folder. The generated file contains the current folder contents as relative paths. In other words, it takes a snapshot of what exists right now and treats those paths as files to preserve later. +> [!IMPORTANT] +> `Generate Keeplist` overwrites the active keeplist file. If you maintain manual keep rules or `!rename` directives, keep a copy or re-add them after generating. + ### Scan -Reads `#keeplist.txt`, evaluates the keep rules, and shows: +Reads the active keeplist, evaluates the keep rules, and shows: - planned file removals - planned rename operations @@ -35,17 +38,25 @@ Reads `#keeplist.txt`, evaluates the keep rules, and shows: Applies the current cleanup plan: -- deletes files not covered by the keeplist +- removes files not covered by the keeplist - applies any valid `!rename` directives -- removes empty parent directories left behind by deleted files +- removes empty parent directories left behind by removed files + +`Clean` supports two modes: + +- `Delete` permanently removes files +- `Quarantine` moves files into `.modcleaner_quarantine//` inside the selected game folder + +Neither mode uses the OS recycle bin. -`Clean` is destructive. Deleted files are not moved to a recycle bin. +> [!WARNING] +> `Delete` permanently removes files. `Quarantine` is safer because it moves removable files into `.modcleaner_quarantine//`, but it is not a full rollback system. ## Intended Workflow 1. Select your game folder. 2. Run `Generate Keeplist` to capture the current files. -3. Edit `#keeplist.txt` if needed. +3. Edit the active keeplist if needed. 4. Run `Scan` to review what would happen. 5. Run `Clean` only after confirming the scan output. @@ -59,22 +70,45 @@ A common pattern is: This tool is snapshot-based. -That means if you install new mods or add files after generating `#keeplist.txt`, those new files are not automatically preserved. If you want to keep them, you must either: +> [!IMPORTANT] +> If you add mods or other files after generating the active keeplist, those files are not automatically preserved. Regenerate the keeplist or add keep rules manually before cleaning. + +If you want to keep them, you must either: - regenerate the keeplist, or - add keep rules manually Also: -- `#keeplist.txt` is never deleted by the cleaner -- `Scan` requires a valid `#keeplist.txt` -- `Clean` requires a valid `#keeplist.txt` +- keeplist files matching `#keeplist.txt` or `#-keeplist.txt` are never deleted by the cleaner. +- `Scan` requires a valid active keeplist +- `Clean` requires a valid active keeplist - an empty keeplist is treated as an error - file ordering and planning are deterministic +### Keeplist Prefixes + +ModCleaner supports an optional in-memory keeplist prefix system. + +- default keeplist: `#keeplist.txt` +- prefixed keeplist: `#-keeplist.txt` + +The prefix UI is intentionally hidden behind `Ctrl+K`. This is to avoid confusion for users who just want a single keeplist file. +If you want to use prefixes, press `Ctrl+K` and enter a prefix to switch to a different keeplist file. You can have as many keeplist files as you want, but only one is active at a time. + +> [!NOTE] +> Keeplist prefixes are an advanced feature. They do not add extra scanning behavior; they only change which keeplist filename `Generate`, `Scan`, and `Clean` use. + +Rules: + +- prefixes are stored in memory only +- an empty prefix returns to `#keeplist.txt` +- valid prefixes may contain only letters, numbers, `_`, and `-` +- invalid prefixes are rejected and do not change the active keeplist + ## Keeplist Format -`#keeplist.txt` is line-based. +The active keeplist is line-based. Blank lines are ignored. @@ -150,7 +184,11 @@ A rename can end in one of these states: Other rename I/O failures are fatal. -There is no rollback for partial progress during `Clean`. (This is why `Scan` is important to review beforehand. This may be improved in the future.) +There is no automatic rollback for partial progress during `Clean`. + +In `Delete` mode, removed files are gone unless you have your own backup or can restore them another way. + +In `Quarantine` mode, removed files are moved into `.modcleaner_quarantine//`, which gives you a manual recovery path, but rename operations and other partial-progress cases are still not rolled back automatically. ## Example Keeplist @@ -168,11 +206,14 @@ mods/** Review the scan output before cleaning. -This tool permanently deletes files that are not matched by the keeplist. It is best used when: +> [!WARNING] +> `Scan` is the review step. `Clean` applies the current plan and does not provide automatic rollback for partial progress. + +This tool can permanently delete files that are not matched by the keeplist. It is best used when: - you understand the folder you are targeting -- you have a backup or can re-verify game files if needed -- you have reviewed `#keeplist.txt` +- you have a backup, can re-verify game files, or are intentionally using quarantine mode +- you have reviewed the active keeplist ## Development @@ -203,4 +244,4 @@ ModCleaner is a deterministic keep-list cleaner for game folders. It does not try to detect which files are mods automatically. Instead, it gives you a simple rule: -If a file is not covered by `#keeplist.txt`, it is removable. +If a file is not covered by the active keeplist, it is removable. diff --git a/logic.ts b/logic.ts index 4babf0d..37caa32 100644 --- a/logic.ts +++ b/logic.ts @@ -57,6 +57,7 @@ export type CleanMode = "delete" | "quarantine"; export type CleanFolderDetailedOptions = { mode?: CleanMode; + keeplistName?: string; }; type KeeplistConfig = { @@ -64,6 +65,14 @@ type KeeplistConfig = { renameDirectives: RenameDirective[]; }; +export function resolveKeeplistName(prefix: string | null): string { + if (!prefix) { + return KEEPLIST_FILE; + } + + return `#${prefix}-keeplist.txt`; +} + export async function walkFiles(root: string): Promise { const files: string[] = []; @@ -76,7 +85,9 @@ export async function walkFiles(root: string): Promise { } if (entry.isDirectory) { const relPath = relative(root, fullPath).replaceAll("\\", "/"); - if (relPath === QUARANTINE_DIR || relPath.startsWith(`${QUARANTINE_DIR}/`)) { + if ( + relPath === QUARANTINE_DIR || relPath.startsWith(`${QUARANTINE_DIR}/`) + ) { continue; } await walk(fullPath); @@ -99,10 +110,13 @@ export async function listRelativeFiles(root: string): Promise { .sort((a, b) => a.localeCompare(b)); } -export async function writeKeeplist(root: string): Promise { +export async function writeKeeplist( + root: string, + keeplistName = KEEPLIST_FILE, +): Promise { const files = await listRelativeFiles(root); const lines = [GENERATED_KEEPLIST_HEADER, "", ...files]; - await Deno.writeTextFile(join(root, KEEPLIST_FILE), `${lines.join("\n")}\n`); + await Deno.writeTextFile(join(root, keeplistName), `${lines.join("\n")}\n`); return files; } @@ -227,29 +241,38 @@ function parseKeeplist(text: string): KeeplistConfig { return { keepRules, renameDirectives }; } -async function readKeeplistConfig(root: string): Promise { - const text = await Deno.readTextFile(join(root, KEEPLIST_FILE)); +async function readKeeplistConfig( + root: string, + keeplistName: string, +): Promise { + const text = await Deno.readTextFile(join(root, keeplistName)); return parseKeeplist(text); } -export async function readKeeplist(root: string): Promise { - const config = await readKeeplistConfig(root); +export async function readKeeplist( + root: string, + keeplistName = KEEPLIST_FILE, +): Promise { + const config = await readKeeplistConfig(root, keeplistName); return config.keepRules; } -async function readValidatedKeeplist(root: string): Promise { +async function readValidatedKeeplist( + root: string, + keeplistName: string, +): Promise { let config: KeeplistConfig; try { - config = await readKeeplistConfig(root); + config = await readKeeplistConfig(root, keeplistName); } catch (error) { if (error instanceof Deno.errors.NotFound) { - throw new Error(`${KEEPLIST_FILE} not found. Generate keeplist first.`); + throw new Error(`${keeplistName} not found. Generate keeplist first.`); } throw error; } if (config.keepRules.length === 0) { - throw new Error(`${KEEPLIST_FILE} is empty. Add at least one keep rule.`); + throw new Error(`${keeplistName} is empty. Add at least one keep rule.`); } return config; @@ -371,10 +394,21 @@ function isKeptWithMatchers(path: string, matchers: RegExp[]): boolean { return matchers.some((matcher) => matcher.test(normalizedPath)); } +function isProtectedKeeplistPath(path: string): boolean { + const normalizedPath = path.replaceAll("\\", "/"); + if (normalizedPath.includes("/")) { + return false; + } + + return normalizedPath === KEEPLIST_FILE || + /^#[A-Za-z0-9_-]+-keeplist\.txt$/.test(normalizedPath); +} + export function getRemovableFiles( files: string[], rules: string[], protectedPaths: Iterable = [], + keeplistName = KEEPLIST_FILE, ): string[] { const matchers = compileKeepRules(rules); const protectedSet = new Set( @@ -382,7 +416,7 @@ export function getRemovableFiles( ); return files.filter((file) => { - if (file === KEEPLIST_FILE) { + if (file === keeplistName || isProtectedKeeplistPath(file)) { return false; } if (protectedSet.has(file.replaceAll("\\", "/"))) { @@ -396,22 +430,29 @@ function protectedPathsFromRenames(renames: RenameDirective[]): string[] { return renames.flatMap((rename) => [rename.from, rename.to]); } -export async function buildScanPlan(root: string): Promise { +export async function buildScanPlan( + root: string, + keeplistName = KEEPLIST_FILE, +): Promise { const files = await listRelativeFiles(root); - const config = await readValidatedKeeplist(root); + const config = await readValidatedKeeplist(root, keeplistName); return { removableFiles: getRemovableFiles( files, config.keepRules, protectedPathsFromRenames(config.renameDirectives), + keeplistName, ), plannedRenames: config.renameDirectives, }; } -export async function scanForRemoval(root: string): Promise { - const plan = await buildScanPlan(root); +export async function scanForRemoval( + root: string, + keeplistName = KEEPLIST_FILE, +): Promise { + const plan = await buildScanPlan(root, keeplistName); return plan.removableFiles; } @@ -491,7 +532,7 @@ export async function cleanFolderDetailed( root: string, options: CleanFolderDetailedOptions = {}, ): Promise { - const plan = await buildScanPlan(root); + const plan = await buildScanPlan(root, options.keeplistName ?? KEEPLIST_FILE); const mode = options.mode ?? "delete"; if (mode !== "delete" && mode !== "quarantine") { @@ -520,7 +561,10 @@ export async function cleanFolderDetailed( }; } -export async function cleanFolder(root: string): Promise { - const result = await cleanFolderDetailed(root); +export async function cleanFolder( + root: string, + keeplistName = KEEPLIST_FILE, +): Promise { + const result = await cleanFolderDetailed(root, { keeplistName }); return result.removedFiles; } diff --git a/logic_test.ts b/logic_test.ts index 3431cb1..fca596f 100644 --- a/logic_test.ts +++ b/logic_test.ts @@ -11,6 +11,7 @@ import { normalizeRelativePath, QUARANTINE_DIR, readKeeplist, + resolveKeeplistName, scanForRemoval, writeKeeplist, } from "./logic.ts"; @@ -68,6 +69,54 @@ Deno.test("getRemovableFiles never removes #keeplist.txt", () => { assertEquals(removable, ["temp/b.log"]); }); +Deno.test("resolveKeeplistName uses default and prefixed keeplist names deterministically", () => { + assertEquals(resolveKeeplistName(null), "#keeplist.txt"); + assertEquals(resolveKeeplistName("vanilla"), "#vanilla-keeplist.txt"); +}); + +Deno.test("getRemovableFiles never removes the active prefixed keeplist", () => { + const files = ["#vanilla-keeplist.txt", "mods/a.txt", "temp/b.log"]; + const removable = getRemovableFiles( + files, + ["mods/**"], + [], + "#vanilla-keeplist.txt", + ); + + assertEquals(removable, ["temp/b.log"]); +}); + +Deno.test("getRemovableFiles protects non-active prefixed keeplists in default mode", () => { + const files = [ + "#keeplist.txt", + "#vanilla-keeplist.txt", + "#modded-keeplist.txt", + "mods/a.txt", + "temp/b.log", + ]; + const removable = getRemovableFiles(files, ["mods/**"]); + + assertEquals(removable, ["temp/b.log"]); +}); + +Deno.test("getRemovableFiles protects sibling keeplists in prefixed mode", () => { + const files = [ + "#keeplist.txt", + "#vanilla-keeplist.txt", + "#modded-keeplist.txt", + "mods/a.txt", + "temp/b.log", + ]; + const removable = getRemovableFiles( + files, + ["mods/**"], + [], + "#vanilla-keeplist.txt", + ); + + assertEquals(removable, ["temp/b.log"]); +}); + Deno.test("writeKeeplist, scanForRemoval and cleanFolder workflow", async () => { const root = await Deno.makeTempDir(); @@ -119,6 +168,35 @@ Deno.test("writeKeeplist prepends generated header verbatim", async () => { } }); +Deno.test("writeKeeplist, scanForRemoval and cleanFolder support prefixed keeplist filenames", async () => { + const root = await Deno.makeTempDir(); + const keeplistName = resolveKeeplistName("vanilla"); + + try { + await Deno.mkdir(join(root, "mods"), { recursive: true }); + await Deno.mkdir(join(root, "logs"), { recursive: true }); + + await Deno.writeTextFile(join(root, "mods", "keep.txt"), "keep"); + await Deno.writeTextFile(join(root, "logs", "delete.log"), "delete"); + + await writeKeeplist(root, keeplistName); + await Deno.writeTextFile(join(root, keeplistName), "mods/**\n"); + + const candidates = await scanForRemoval(root, keeplistName); + assertEquals(candidates, ["logs/delete.log"]); + + const removed = await cleanFolder(root, keeplistName); + assertEquals(removed, ["logs/delete.log"]); + + const keeplistExists = await Deno.stat(join(root, keeplistName)) + .then(() => true) + .catch(() => false); + assertEquals(keeplistExists, true); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("listRelativeFiles returns deterministic lexicographic ordering", async () => { const root = await Deno.makeTempDir(); @@ -190,6 +268,22 @@ Deno.test("scanForRemoval fails when keeplist is missing", async () => { } }); +Deno.test("scanForRemoval reports the prefixed keeplist name when it is missing", async () => { + const root = await Deno.makeTempDir(); + const keeplistName = resolveKeeplistName("vanilla"); + + try { + await Deno.writeTextFile(join(root, "orphan.txt"), "x"); + await assertRejects( + () => scanForRemoval(root, keeplistName), + Error, + "#vanilla-keeplist.txt not found", + ); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("cleanFolder fails when keeplist is empty", async () => { const root = await Deno.makeTempDir(); try { @@ -205,6 +299,23 @@ Deno.test("cleanFolder fails when keeplist is empty", async () => { } }); +Deno.test("cleanFolder fails when prefixed keeplist is empty", async () => { + const root = await Deno.makeTempDir(); + const keeplistName = resolveKeeplistName("vanilla"); + + try { + await Deno.writeTextFile(join(root, "orphan.txt"), "x"); + await Deno.writeTextFile(join(root, keeplistName), "\n"); + await assertRejects( + () => cleanFolder(root, keeplistName), + Error, + "#vanilla-keeplist.txt is empty", + ); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("readKeeplist ignores '# ' comments but keeps literal # paths", async () => { const root = await Deno.makeTempDir(); try { @@ -506,20 +617,37 @@ Deno.test("listRelativeFiles and buildScanPlan exclude quarantine directory", as try { await Deno.mkdir(join(root, "keep"), { recursive: true }); - await Deno.mkdir(join(root, QUARANTINE_DIR, "2026-01-01T00-00-00.000Z", "trash"), { recursive: true }); + await Deno.mkdir( + join(root, QUARANTINE_DIR, "2026-01-01T00-00-00.000Z", "trash"), + { recursive: true }, + ); await Deno.writeTextFile(join(root, "keep", "stay.txt"), "keep"); await Deno.writeTextFile( - join(root, QUARANTINE_DIR, "2026-01-01T00-00-00.000Z", "trash", "old.tmp"), + join( + root, + QUARANTINE_DIR, + "2026-01-01T00-00-00.000Z", + "trash", + "old.tmp", + ), "old", ); await Deno.writeTextFile(join(root, KEEPLIST_FILE), "keep/**\n"); const files = await listRelativeFiles(root); - assertEquals(files.includes(`${QUARANTINE_DIR}/2026-01-01T00-00-00.000Z/trash/old.tmp`), false); + assertEquals( + files.includes( + `${QUARANTINE_DIR}/2026-01-01T00-00-00.000Z/trash/old.tmp`, + ), + false, + ); const plan = await buildScanPlan(root); - assertEquals(plan.removableFiles.some((f) => f.startsWith(QUARANTINE_DIR)), false); + assertEquals( + plan.removableFiles.some((f) => f.startsWith(QUARANTINE_DIR)), + false, + ); } finally { await Deno.remove(root, { recursive: true }); } diff --git a/main.ts b/main.ts index 69f3f87..abcdd40 100644 --- a/main.ts +++ b/main.ts @@ -5,6 +5,7 @@ import { cleanFolderDetailed, type CleanMode, QUARANTINE_DIR, + resolveKeeplistName, writeKeeplist, } from "./logic.ts"; @@ -162,6 +163,35 @@ button.danger:hover:not(:disabled) { display: flex; gap: 10px; } +.prefix-toolbar { + margin-top: 12px; + display: flex; + justify-content: flex-end; +} +.prefix-active { + color: var(--muted); + font-size: 13px; +} +.prefix-panel { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); +} +.prefix-row { + display: flex; + gap: 10px; + align-items: end; +} +.prefix-input-wrap { + flex: 1; +} +.prefix-error { + margin-top: 8px; + color: #f0a3a3; + font-size: 13px; +} .mode-row { margin-top: 12px; display: flex; @@ -307,6 +337,10 @@ button.danger:hover:not(:disabled) { .actions { flex-wrap: wrap; } + .prefix-row { + flex-direction: column; + align-items: stretch; + } .actions .danger-wrap { margin-left: 0; padding-left: 0; @@ -323,7 +357,7 @@ button.danger:hover:not(:disabled) {

ModCleaner

-
Scan and clean game folders using #keeplist.txt rules.
+
Scan and clean game folders using the active keeplist rules.
@@ -333,6 +367,21 @@ button.danger:hover:not(:disabled) {
+
+
Active keeplist: #keeplist.txt
+
+ + +
@@ -356,23 +405,23 @@ button.danger:hover:not(:disabled) { How It Works
This tool uses a snapshot-style keep system.
-
Only files matching #keeplist.txt rules are preserved.
-
If you install new mods or add files you want to keep, update #keeplist.txt first.
+
Only files matching the active keeplist rules are preserved.
+
If you install new mods or add files you want to keep, update the active keeplist first.
You can do that by running Generate Keeplist again or adding wildcard rules manually.

-
Generate Keeplist writes #keeplist.txt into the selected game folder.
+
Generate Keeplist writes the active keeplist into the selected game folder.
Each line is a keep rule. Files matching any rule are preserved.
Supported patterns:
* matches within one path segment. Example: BepInEx/plugins/*.dll
** matches across nested folders. Example: mods/**
Trailing slash is treated as recursive. Example: mods/ equals mods/**
-
#keeplist.txt is overwritten each time you generate. Add manual wildcard rules after generating if needed.
+
The active keeplist is overwritten each time you generate. Add manual wildcard rules after generating if needed.
Every non-empty line is treated as a rule. Lines starting with # are comments. Blank lines are ignored.
Literal rename directives are supported: !rename Game/Binaries/Win64/exchndl-original.dll -> Game/Binaries/Win64/exchndl.dll
!rename paths are relative-only and do not support wildcards.

-
Scan checks the folder against #keeplist.txt and lists files that would be removed.
-
Scan and Clean require #keeplist.txt to exist and contain at least one rule.
+
Scan checks the folder against the active keeplist and lists files that would be removed.
+
Scan and Clean require the active keeplist to exist and contain at least one rule.
Clean can permanently delete files or move them into ${QUARANTINE_DIR}/<timestamp>.
@@ -401,6 +450,86 @@ button.danger:hover:not(:disabled) { let confirmResolver = null; let cleanReady = false; let busy = false; +let keeplistPrefix = null; +let prefixValidationError = ""; + +function getResolvedKeeplistName() { + return keeplistPrefix ? \`#\${keeplistPrefix}-keeplist.txt\` : "#keeplist.txt"; +} + +function getPrefixValidationError() { + return prefixValidationError; +} + +function updateActiveKeeplistName() { + const el = document.getElementById("activeKeeplistName"); + if (!el) { + return; + } + + el.innerHTML = \`Active keeplist: \${getResolvedKeeplistName()}\`; +} + +function setPrefixValidationError(message) { + prefixValidationError = message; + const el = document.getElementById("prefixError"); + if (!el) { + return; + } + + el.innerText = message; + el.hidden = !message; +} + +function applyPrefixValue(rawValue) { + const trimmed = rawValue.trim(); + if (!trimmed) { + keeplistPrefix = null; + setPrefixValidationError(""); + updateActiveKeeplistName(); + return true; + } + + const normalized = trimmed.toLowerCase(); + if (!/^[a-z0-9_-]+$/.test(normalized)) { + setPrefixValidationError("Prefix must use only letters, numbers, underscores, or hyphens."); + updateActiveKeeplistName(); + return false; + } + + keeplistPrefix = normalized; + setPrefixValidationError(""); + updateActiveKeeplistName(); + return true; +} + +function handlePrefixInput() { + applyPrefixValue(document.getElementById("keeplistPrefix")?.value || ""); + markScanDirty(); +} + +function clearPrefix() { + const input = document.getElementById("keeplistPrefix"); + if (input) { + input.value = ""; + } + keeplistPrefix = null; + setPrefixValidationError(""); + updateActiveKeeplistName(); + markScanDirty(); +} + +function togglePrefixPanel() { + const panel = document.getElementById("prefixPanel"); + if (!panel) { + return; + } + + panel.hidden = !panel.hidden; + if (!panel.hidden) { + document.getElementById("keeplistPrefix")?.focus(); + } +} function setStatus(msg, type = "info") { document.getElementById("status").innerText = msg; @@ -408,7 +537,13 @@ function setStatus(msg, type = "info") { } function setBusy(isBusy) { busy = Boolean(isBusy); - const ids = ["generateButton", "scanButton", "cleanButton"]; + const ids = [ + "generateButton", + "scanButton", + "cleanButton", + "prefixCancelButton", + "keeplistPrefix", + ]; ids.forEach((id) => { const el = document.getElementById(id); if (el) { @@ -518,18 +653,22 @@ document.getElementById("confirmModal").addEventListener("click", (event) => { document.addEventListener("keydown", (event) => { const modal = document.getElementById("confirmModal"); - if (modal.hidden) { + if (!modal.hidden && event.key === "Escape") { + event.preventDefault(); + resolveConfirm("false"); return; } - if (event.key === "Escape") { + + if (event.ctrlKey && event.key.toLowerCase() === "k") { event.preventDefault(); - resolveConfirm("false"); + togglePrefixPanel(); } }); setResults({ removals: [], renames: [] }); setCleanReady(false); updateCleanModeHelp(); +updateActiveKeeplistName(); @@ -553,10 +692,48 @@ function runCleanReady(event: WebUI.Event, isReady: boolean): void { event.window.run(`setCleanReady(${JSON.stringify(isReady)})`); } -async function showCleanupConfirmation(event: WebUI.Event): Promise { +async function getKeeplistState( + event: WebUI.Event, +): Promise<{ keeplistName: string; prefixError: string }> { + const value = await event.window.script(` + return { + keeplistName: getResolvedKeeplistName(), + prefixError: getPrefixValidationError(), + }; + `); + + if (typeof value !== "object" || value === null) { + return { keeplistName: resolveKeeplistName(null), prefixError: "" }; + } + + const state = value as Record; + const keeplistName = typeof state.keeplistName === "string" + ? state.keeplistName + : resolveKeeplistName(null); + const prefixError = typeof state.prefixError === "string" + ? state.prefixError + : ""; + + return { keeplistName, prefixError }; +} + +async function requireKeeplistName(event: WebUI.Event): Promise { + const { keeplistName, prefixError } = await getKeeplistState(event); + if (prefixError) { + runStatus(event, `Invalid keeplist prefix: ${prefixError}`, "error"); + return null; + } + + return keeplistName; +} + +async function showCleanupConfirmation( + event: WebUI.Event, + keeplistName: string, +): Promise { const mode = await getCleanMode(event); const message = mode === "delete" - ? "Delete mode permanently removes files that are not covered by #keeplist.txt. Continue?" + ? `Delete mode permanently removes files that are not covered by ${keeplistName}. Continue?` : `Quarantine mode moves removable files to ${QUARANTINE_DIR}// inside the selected game folder. Continue?`; const confirmedValue: unknown = await event.window.script(` @@ -600,6 +777,7 @@ async function getRootPath(event: WebUI.Event): Promise { } let lastScannedRoot: string | null = null; +let lastScannedKeeplistName: string | null = null; async function browseFolder(event: WebUI.Event): Promise { const currentPath = event.arg.string(0).trim(); @@ -623,6 +801,7 @@ async function browseFolder(event: WebUI.Event): Promise { }; markScanDirty();`, ); lastScannedRoot = null; + lastScannedKeeplistName = null; runCleanReady(event, false); runStatus(event, "Folder selected", "success"); } catch (error) { @@ -639,14 +818,20 @@ async function generateKeeplist(event: WebUI.Event): Promise { return; } + const keeplistName = await requireKeeplistName(event); + if (!keeplistName) { + return; + } + try { runBusy(event, true); - const files = await writeKeeplist(root); + const files = await writeKeeplist(root, keeplistName); lastScannedRoot = null; + lastScannedKeeplistName = null; runCleanReady(event, false); runStatus( event, - `#keeplist.txt generated (${files.length} entries)`, + `${keeplistName} generated (${files.length} entries)`, "success", ); } catch (error) { @@ -663,10 +848,16 @@ async function scanFolder(event: WebUI.Event): Promise { return; } + const keeplistName = await requireKeeplistName(event); + if (!keeplistName) { + return; + } + try { runBusy(event, true); - const plan = await buildScanPlan(root); + const plan = await buildScanPlan(root, keeplistName); lastScannedRoot = root; + lastScannedKeeplistName = keeplistName; runCleanReady(event, true); runResults(event, plan.removableFiles, plan.plannedRenames); runStatus( @@ -676,6 +867,7 @@ async function scanFolder(event: WebUI.Event): Promise { ); } catch (error) { lastScannedRoot = null; + lastScannedKeeplistName = null; runCleanReady(event, false); runStatus(event, `Failed to scan folder: ${String(error)}`, "error"); } finally { @@ -690,7 +882,12 @@ async function cleanFiles(event: WebUI.Event): Promise { return; } - if (lastScannedRoot !== root) { + const keeplistName = await requireKeeplistName(event); + if (!keeplistName) { + return; + } + + if (lastScannedRoot !== root || lastScannedKeeplistName !== keeplistName) { runCleanReady(event, false); runStatus( event, @@ -700,7 +897,7 @@ async function cleanFiles(event: WebUI.Event): Promise { return; } - const confirmed = await showCleanupConfirmation(event); + const confirmed = await showCleanupConfirmation(event, keeplistName); if (!confirmed) { runStatus(event, "Cleanup cancelled", "info"); return; @@ -710,7 +907,7 @@ async function cleanFiles(event: WebUI.Event): Promise { try { runBusy(event, true); - const result = await cleanFolderDetailed(root, { mode }); + const result = await cleanFolderDetailed(root, { mode, keeplistName }); const appliedRenames = result.renameResults.filter((rename) => rename.applied ) @@ -741,6 +938,7 @@ async function cleanFiles(event: WebUI.Event): Promise { runStatus(event, `Failed to clean folder: ${String(error)}`, "error"); } finally { lastScannedRoot = null; + lastScannedKeeplistName = null; runCleanReady(event, false); runBusy(event, false); } From ca7a58a0aee290d60fcdb17b57bc81a37e605be7 Mon Sep 17 00:00:00 2001 From: Theros Date: Sun, 1 Mar 2026 13:50:20 +0000 Subject: [PATCH 6/8] fix: handle JSON parsing errors in getKeeplistState function --- main.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/main.ts b/main.ts index abcdd40..f4ffbfa 100644 --- a/main.ts +++ b/main.ts @@ -696,17 +696,27 @@ async function getKeeplistState( event: WebUI.Event, ): Promise<{ keeplistName: string; prefixError: string }> { const value = await event.window.script(` - return { + return JSON.stringify({ keeplistName: getResolvedKeeplistName(), prefixError: getPrefixValidationError(), - }; + }); `); - if (typeof value !== "object" || value === null) { + if (typeof value !== "string") { + return { keeplistName: resolveKeeplistName(null), prefixError: "" }; + } + + let state: Record; + try { + const parsed = JSON.parse(value); + if (typeof parsed !== "object" || parsed === null) { + return { keeplistName: resolveKeeplistName(null), prefixError: "" }; + } + state = parsed as Record; + } catch { return { keeplistName: resolveKeeplistName(null), prefixError: "" }; } - const state = value as Record; const keeplistName = typeof state.keeplistName === "string" ? state.keeplistName : resolveKeeplistName(null); From 0d32d44ea87f9b1ed0ef4728def7fe3f5c9957c7 Mon Sep 17 00:00:00 2001 From: Theros Date: Sun, 1 Mar 2026 14:07:56 +0000 Subject: [PATCH 7/8] feat: add validation for keeplist names and corresponding tests --- logic.ts | 15 +++++++++++++ logic_test.ts | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/logic.ts b/logic.ts index 37caa32..ab82124 100644 --- a/logic.ts +++ b/logic.ts @@ -65,6 +65,18 @@ type KeeplistConfig = { renameDirectives: RenameDirective[]; }; +function validateKeeplistName(keeplistName: string): void { + keeplistName = keeplistName.trim(); + if ( + keeplistName !== KEEPLIST_FILE && + !/^#[A-Za-z0-9_-]+-keeplist\.txt$/.test(keeplistName) + ) { + throw new Error( + `Invalid keeplist name: ${keeplistName}. Expected #keeplist.txt or #-keeplist.txt.`, + ); + } +} + export function resolveKeeplistName(prefix: string | null): string { if (!prefix) { return KEEPLIST_FILE; @@ -114,6 +126,7 @@ export async function writeKeeplist( root: string, keeplistName = KEEPLIST_FILE, ): Promise { + validateKeeplistName(keeplistName); const files = await listRelativeFiles(root); const lines = [GENERATED_KEEPLIST_HEADER, "", ...files]; await Deno.writeTextFile(join(root, keeplistName), `${lines.join("\n")}\n`); @@ -245,6 +258,7 @@ async function readKeeplistConfig( root: string, keeplistName: string, ): Promise { + validateKeeplistName(keeplistName); const text = await Deno.readTextFile(join(root, keeplistName)); return parseKeeplist(text); } @@ -434,6 +448,7 @@ export async function buildScanPlan( root: string, keeplistName = KEEPLIST_FILE, ): Promise { + validateKeeplistName(keeplistName); const files = await listRelativeFiles(root); const config = await readValidatedKeeplist(root, keeplistName); diff --git a/logic_test.ts b/logic_test.ts index fca596f..be980f7 100644 --- a/logic_test.ts +++ b/logic_test.ts @@ -197,6 +197,26 @@ Deno.test("writeKeeplist, scanForRemoval and cleanFolder support prefixed keepli } }); +Deno.test("writeKeeplist rejects invalid keeplist names before filesystem writes", async () => { + const root = await Deno.makeTempDir(); + const escapedPath = join(root, "..", "escaped-keeplist.txt"); + + try { + await assertRejects( + () => writeKeeplist(root, "../escaped-keeplist.txt"), + Error, + "Invalid keeplist name: ../escaped-keeplist.txt.", + ); + + const escapedExists = await Deno.stat(escapedPath) + .then(() => true) + .catch(() => false); + assertEquals(escapedExists, false); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("listRelativeFiles returns deterministic lexicographic ordering", async () => { const root = await Deno.makeTempDir(); @@ -284,6 +304,34 @@ Deno.test("scanForRemoval reports the prefixed keeplist name when it is missing" } }); +Deno.test("readKeeplist rejects invalid keeplist names", async () => { + const root = await Deno.makeTempDir(); + + try { + await assertRejects( + () => readKeeplist(root, "#../keeplist.txt"), + Error, + "Invalid keeplist name: #../keeplist.txt.", + ); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + +Deno.test("scanForRemoval rejects invalid keeplist names", async () => { + const root = await Deno.makeTempDir(); + + try { + await assertRejects( + () => scanForRemoval(root, "#/bad-keeplist.txt"), + Error, + "Invalid keeplist name: #/bad-keeplist.txt.", + ); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("cleanFolder fails when keeplist is empty", async () => { const root = await Deno.makeTempDir(); try { @@ -299,6 +347,20 @@ Deno.test("cleanFolder fails when keeplist is empty", async () => { } }); +Deno.test("cleanFolder rejects invalid keeplist names", async () => { + const root = await Deno.makeTempDir(); + + try { + await assertRejects( + () => cleanFolder(root, "C:/outside/#keeplist.txt"), + Error, + "Invalid keeplist name: C:/outside/#keeplist.txt.", + ); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("cleanFolder fails when prefixed keeplist is empty", async () => { const root = await Deno.makeTempDir(); const keeplistName = resolveKeeplistName("vanilla"); From f984bf43df142fc5f52959ca6edde2f8930a6f6f Mon Sep 17 00:00:00 2001 From: Theros Date: Sun, 1 Mar 2026 14:27:43 +0000 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- logic.ts | 16 ++++++++++++---- main.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/logic.ts b/logic.ts index ab82124..c38260f 100644 --- a/logic.ts +++ b/logic.ts @@ -66,13 +66,21 @@ type KeeplistConfig = { }; function validateKeeplistName(keeplistName: string): void { - keeplistName = keeplistName.trim(); + // Reject names with leading or trailing whitespace so that validation + // and subsequent filesystem operations use the same literal value. + if (keeplistName !== keeplistName.trim()) { + throw new Error( + `Invalid keeplist name: ${keeplistName}. Leading or trailing whitespace is not allowed.`, + ); + } + + const normalized = keeplistName.trim(); if ( - keeplistName !== KEEPLIST_FILE && - !/^#[A-Za-z0-9_-]+-keeplist\.txt$/.test(keeplistName) + normalized !== KEEPLIST_FILE && + !/^#[A-Za-z0-9_-]+-keeplist\.txt$/.test(normalized) ) { throw new Error( - `Invalid keeplist name: ${keeplistName}. Expected #keeplist.txt or #-keeplist.txt.`, + `Invalid keeplist name: ${normalized}. Expected #keeplist.txt or #-keeplist.txt.`, ); } } diff --git a/main.ts b/main.ts index f4ffbfa..9ebc5f9 100644 --- a/main.ts +++ b/main.ts @@ -936,8 +936,12 @@ async function cleanFiles(event: WebUI.Event): Promise { to: rename.to, })), ); - const modeSummary = result.mode === "quarantine" && result.quarantineRunId - ? `quarantined at ${QUARANTINE_DIR}/${result.quarantineRunId}` + const modeSummary = result.removedFiles.length === 0 + ? "not removed (no files to clean)" + : result.mode === "quarantine" + ? (result.quarantineRunId + ? `quarantined at ${QUARANTINE_DIR}/${result.quarantineRunId}` + : "quarantined") : "deleted permanently"; runStatus( event,