Created
June 1, 2026 05:33
-
-
Save sorrycc/7b944a76eea7c895dad4cd4538b80158 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bun | |
| import { createInterface } from "node:readline/promises"; | |
| import { stdin as input, stdout as output } from "node:process"; | |
| import { existsSync } from "node:fs"; | |
| import { cp, lstat, mkdir, readdir, rename, rm } from "node:fs/promises"; | |
| import { spawnSync } from "node:child_process"; | |
| import os from "node:os"; | |
| import path from "node:path"; | |
| const home = os.homedir(); | |
| const configDir = (process.env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude")).normalize("NFC"); | |
| const args = process.argv.slice(2); | |
| const timestamp = new Date().toISOString().replace(/[.:]/g, "-"); | |
| const backupRoot = path.join(home, "Downloads", "claude-code-cleanup-backups", timestamp); | |
| type TargetKind = "safe" | "debug" | "config" | "auth" | "project" | "native" | "reset"; | |
| type Target = { | |
| path: string; | |
| reason: string; | |
| kind: TargetKind; | |
| }; | |
| type KeychainEntry = { | |
| service: string; | |
| account: string; | |
| }; | |
| function has(flag: string) { | |
| return args.includes(flag); | |
| } | |
| function valueOf(flag: string) { | |
| const index = args.indexOf(flag); | |
| if (index === -1) return undefined; | |
| return args[index + 1]; | |
| } | |
| function usage() { | |
| console.log(`Usage: | |
| bun ~/Downloads/clean-claude-code-data.ts [options] | |
| Default is dry-run. Nothing changes unless you pass --apply. | |
| The script walks ~/.claude and classifies each entry. Anything not on the | |
| known list is treated as bloat (kind: safe, reason: "unknown entry"), so new | |
| data dirs Anthropic adds are cleaned automatically. Authored content | |
| (CLAUDE.md, agents/, commands/, skills/, plans/, ...) and auth are kept | |
| unless you opt in to remove them. | |
| Common: | |
| --apply Move selected data into a timestamped backup folder | |
| --delete Permanently delete selected data instead of moving it | |
| --yes Skip confirmation prompt | |
| --project <path> Also clean local Claude Code data in one project | |
| Tiers (each is opt-in beyond the default safe tier): | |
| --include-debug Debug logs, telemetry, daemon state, IDE state | |
| --include-config Settings, CLAUDE.md, skills, agents, commands, | |
| plugins, hooks, output styles, teams, keybindings, | |
| scheduled tasks, agent memory, plans, config backups | |
| --include-auth Local credential files (.credentials.json). Will | |
| sign you out of API key / OAuth on next launch. | |
| --include-keychain (macOS) Also remove Claude Code Keychain entries. | |
| IRREVERSIBLE — implies --include-auth. | |
| --include-native-cache Native install caches: ~/.local/share/claude, | |
| ~/.cache/claude, ~/.local/state/claude | |
| --uninstall-native Also remove the native binary: ~/.local/bin/claude. | |
| Uninstalls Claude Code from this account. | |
| Nuclear: | |
| --reset Move/delete the whole config dir and ~/.claude.json | |
| --help Show this help | |
| Honors $CLAUDE_CONFIG_DIR (default: ~/.claude). | |
| Current config dir: ${configDir} | |
| Recommended: | |
| bun ~/Downloads/clean-claude-code-data.ts | |
| bun ~/Downloads/clean-claude-code-data.ts --apply | |
| Aggressive reset: | |
| bun ~/Downloads/clean-claude-code-data.ts --reset --apply | |
| Permanent delete: | |
| bun ~/Downloads/clean-claude-code-data.ts --apply --delete --yes | |
| `); | |
| } | |
| async function dirSize(targetPath: string): Promise<number> { | |
| let info; | |
| try { | |
| info = await lstat(targetPath); | |
| } catch { | |
| return 0; | |
| } | |
| if (info.isSymbolicLink()) return 0; | |
| if (!info.isDirectory()) return info.size; | |
| let total = 0; | |
| let entries: string[] = []; | |
| try { | |
| entries = await readdir(targetPath); | |
| } catch { | |
| return info.size; | |
| } | |
| for (const entry of entries) { | |
| total += await dirSize(path.join(targetPath, entry)); | |
| } | |
| return total; | |
| } | |
| function formatBytes(bytes: number) { | |
| if (bytes === 0) return "0 B"; | |
| const units = ["B", "KB", "MB", "GB", "TB"]; | |
| const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); | |
| return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`; | |
| } | |
| function pathInside(targetPath: string, root: string) { | |
| const resolved = path.resolve(targetPath); | |
| const resolvedRoot = path.resolve(root); | |
| return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`); | |
| } | |
| async function isSymlink(targetPath: string) { | |
| try { | |
| const info = await lstat(targetPath); | |
| return info.isSymbolicLink(); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| function backupPathFor(targetPath: string) { | |
| const resolved = path.resolve(targetPath); | |
| const relative = path.relative(path.parse(resolved).root, resolved); | |
| return path.join(backupRoot, relative); | |
| } | |
| async function moveToBackup(targetPath: string) { | |
| const destination = backupPathFor(targetPath); | |
| await mkdir(path.dirname(destination), { recursive: true }); | |
| try { | |
| await rename(targetPath, destination); | |
| } catch (error) { | |
| const code = (error as NodeJS.ErrnoException).code; | |
| if (code !== "EXDEV") throw error; | |
| await cp(targetPath, destination, { recursive: true, force: false, errorOnExist: true }); | |
| await rm(targetPath, { recursive: true, force: true }); | |
| } | |
| return destination; | |
| } | |
| // Classification for entries directly inside the config dir (~/.claude). | |
| // Anything not listed falls through to "safe" — i.e. new files Anthropic | |
| // adds get cleaned by default. Surface them in the dry-run output so the | |
| // user notices unexpected new entries before --apply. | |
| type EntrySpec = { kind: "safe" | "debug" | "config" | "auth"; reason: string }; | |
| const KNOWN_ENTRIES: Record<string, EntrySpec> = { | |
| // safe — generated local data and history Claude can rebuild | |
| projects: { kind: "safe", reason: "project transcripts and session history" }, | |
| todos: { kind: "safe", reason: "todo state" }, | |
| tasks: { kind: "safe", reason: "task state (modern sibling of todos)" }, | |
| "shell-snapshots": { kind: "safe", reason: "shell environment snapshots" }, | |
| sessions: { kind: "safe", reason: "session metadata" }, | |
| jobs: { kind: "safe", reason: "background job state" }, | |
| "session-env": { kind: "safe", reason: "per-session environment captures" }, | |
| "file-history": { kind: "safe", reason: "backups of files Claude edited" }, | |
| "paste-cache": { kind: "safe", reason: "pasted-content cache" }, | |
| "image-cache": { kind: "safe", reason: "image cache" }, | |
| uploads: { kind: "safe", reason: "inbound attachment uploads" }, | |
| cache: { kind: "safe", reason: "general cache (changelog, install counts, etc.)" }, | |
| downloads: { kind: "safe", reason: "downloaded assets" }, | |
| "history.jsonl": { kind: "safe", reason: "prompt history" }, | |
| "stats-cache.json": { kind: "safe", reason: "stats cache" }, | |
| "mcp-needs-auth-cache.json": { kind: "safe", reason: "MCP auth-prompt cache" }, | |
| "install-counts-cache.json": { kind: "safe", reason: "plugin install-counts cache" }, | |
| "server-sessions.json": { kind: "safe", reason: "server session metadata" }, | |
| ".version-cleanup": { kind: "safe", reason: "version cleanup marker" }, | |
| ".npm-cache-cleanup": { kind: "safe", reason: "npm cache cleanup marker" }, | |
| ".update.lock": { kind: "safe", reason: "auto-update lock" }, | |
| // debug — noisy logs and telemetry; opt-in via --include-debug | |
| debug: { kind: "debug", reason: "per-session debug logs (can be huge)" }, | |
| statsig: { kind: "debug", reason: "Statsig feature-flag/telemetry cache" }, | |
| telemetry: { kind: "debug", reason: "telemetry events" }, | |
| ide: { kind: "debug", reason: "IDE integration state" }, | |
| channels: { kind: "debug", reason: "messaging channels state" }, | |
| pets: { kind: "debug", reason: "pets state" }, | |
| daemon: { kind: "debug", reason: "daemon runtime state" }, | |
| "daemon.lock": { kind: "debug", reason: "daemon lock file" }, | |
| "daemon.log": { kind: "debug", reason: "daemon log" }, | |
| "daemon.status.json": { kind: "debug", reason: "daemon status" }, | |
| "daemon-auth-cooldown": { kind: "debug", reason: "daemon auth cooldown state" }, | |
| "daemon-auth-status.json": { kind: "debug", reason: "daemon auth status" }, | |
| // config — settings, extensions, authored content; opt-in via --include-config | |
| "settings.json": { kind: "config", reason: "global Claude Code settings" }, | |
| "settings.local.json": { kind: "config", reason: "global local Claude Code settings" }, | |
| "settings.json.bak": { kind: "config", reason: "settings backup" }, | |
| "managed-settings.json": { kind: "config", reason: "managed settings" }, | |
| "remote-settings.json": { kind: "config", reason: "remote settings" }, | |
| "policy-limits.json": { kind: "config", reason: "policy limits" }, | |
| "model-capabilities.json": { kind: "config", reason: "model capabilities cache" }, | |
| "config.json": { kind: "config", reason: "legacy global config" }, | |
| ".config.json": { kind: "config", reason: "legacy global config (dot-prefixed)" }, | |
| "CLAUDE.md": { kind: "config", reason: "global Claude instructions" }, | |
| "MEMORY.md": { kind: "config", reason: "global Claude memory" }, | |
| rules: { kind: "config", reason: "global user rules" }, | |
| plans: { kind: "config", reason: "saved plans" }, | |
| backups: { kind: "config", reason: "config backups directory" }, | |
| commands: { kind: "config", reason: "custom slash commands" }, | |
| agents: { kind: "config", reason: "custom agents" }, | |
| "agent-memory": { kind: "config", reason: "agent memory" }, | |
| "agent-memory-local": { kind: "config", reason: "agent memory (local)" }, | |
| skills: { kind: "config", reason: "installed skills" }, | |
| plugins: { kind: "config", reason: "installed plugins" }, | |
| "installed_plugins.json": { kind: "config", reason: "plugin registry (legacy)" }, | |
| "installed_plugins_v2.json": { kind: "config", reason: "plugin registry" }, | |
| "known_marketplaces.json": { kind: "config", reason: "known plugin marketplaces" }, | |
| "flagged-plugins.json": { kind: "config", reason: "flagged plugins list" }, | |
| hooks: { kind: "config", reason: "hooks directory" }, | |
| "hooks.json": { kind: "config", reason: "hooks config" }, | |
| "output-styles": { kind: "config", reason: "output styles" }, | |
| teams: { kind: "config", reason: "teams config" }, | |
| "keybindings.json": { kind: "config", reason: "custom keybindings" }, | |
| "scheduled_tasks.json": { kind: "config", reason: "scheduled tasks (cron/wakeups)" }, | |
| "scheduled_tasks.lock": { kind: "config", reason: "scheduled tasks lock" }, | |
| "statusline-command.sh": { kind: "config", reason: "custom status line script" }, | |
| // auth — opt-in via --include-auth | |
| ".credentials.json": { kind: "auth", reason: "OAuth/API credentials (plain-text fallback)" }, | |
| }; | |
| function classifyEntry(name: string): EntrySpec { | |
| const known = KNOWN_ENTRIES[name]; | |
| if (known) return known; | |
| // Pattern fallbacks for variable filenames. | |
| if (name.startsWith("mcp-refresh-") && name.endsWith(".lock")) { | |
| return { kind: "safe", reason: "MCP refresh lock" }; | |
| } | |
| return { kind: "safe", reason: "unknown entry (treated as bloat)" }; | |
| } | |
| async function scanConfigDir(): Promise<Target[]> { | |
| let entries: string[] = []; | |
| try { | |
| entries = await readdir(configDir); | |
| } catch { | |
| return []; | |
| } | |
| const targets: Target[] = []; | |
| for (const name of entries) { | |
| const spec = classifyEntry(name); | |
| targets.push({ path: path.join(configDir, name), reason: spec.reason, kind: spec.kind }); | |
| } | |
| return targets; | |
| } | |
| function outOfDirPaths() { | |
| return { | |
| // Config tier files outside the config dir (legacy state at $HOME). | |
| config: [ | |
| { path: path.join(home, ".claude.json"), reason: "legacy Claude Code state file, if present" }, | |
| { path: path.join(home, ".claude.json.backup"), reason: "legacy Claude Code state backup, if present" }, | |
| ], | |
| // --include-native-cache: installer/runtime data outside the config dir. | |
| native: [ | |
| { path: path.join(home, ".local", "share", "claude"), reason: "native install data" }, | |
| { path: path.join(home, ".cache", "claude"), reason: "native install cache" }, | |
| { path: path.join(home, ".local", "state", "claude"), reason: "native install state" }, | |
| ], | |
| // --uninstall-native: the binary itself. | |
| nativeBinary: [ | |
| { path: path.join(home, ".local", "bin", "claude"), reason: "native Claude Code binary" }, | |
| ], | |
| }; | |
| } | |
| function projectPaths(resolvedProject: string, includeConfig: boolean) { | |
| const safe: Array<{ path: string; reason: string }> = [ | |
| { path: path.join(resolvedProject, ".claude", "todos"), reason: "project-local todo state" }, | |
| { path: path.join(resolvedProject, ".claude", "tasks"), reason: "project-local task state" }, | |
| { path: path.join(resolvedProject, ".claude", "shell-snapshots"), reason: "project-local shell snapshots" }, | |
| { path: path.join(resolvedProject, ".claude", "projects"), reason: "project-local transcripts" }, | |
| { path: path.join(resolvedProject, ".claude", "plans"), reason: "project-local plans" }, | |
| { path: path.join(resolvedProject, ".claude", "worktrees"), reason: "project-local worktree metadata" }, | |
| { path: path.join(resolvedProject, ".claude", "local"), reason: "project-local scratch" }, | |
| { path: path.join(resolvedProject, ".claude", "remote"), reason: "project-local remote cache" }, | |
| { path: path.join(resolvedProject, ".claude", "bash-log.txt"), reason: "project-local bash log" }, | |
| { path: path.join(resolvedProject, ".claude", "agent-memory-snapshots"), reason: "project-local agent memory snapshots" }, | |
| ]; | |
| const config: Array<{ path: string; reason: string }> = [ | |
| { path: path.join(resolvedProject, ".claude", "settings.json"), reason: "project Claude Code settings" }, | |
| { path: path.join(resolvedProject, ".claude", "settings.local.json"), reason: "project-local Claude Code settings" }, | |
| { path: path.join(resolvedProject, ".claude", "commands"), reason: "project commands" }, | |
| { path: path.join(resolvedProject, ".claude", "agents"), reason: "project agents" }, | |
| { path: path.join(resolvedProject, ".claude", "skills"), reason: "project skills" }, | |
| { path: path.join(resolvedProject, ".claude", "hooks"), reason: "project hooks" }, | |
| { path: path.join(resolvedProject, ".claude", "output-styles"), reason: "project output styles" }, | |
| { path: path.join(resolvedProject, ".claude", "agent-memory"), reason: "project-local agent memory" }, | |
| { path: path.join(resolvedProject, ".claude", "agent-memory-local"), reason: "project-local agent memory (local)" }, | |
| { path: path.join(resolvedProject, ".claude", "teams"), reason: "project teams config" }, | |
| { path: path.join(resolvedProject, ".claude", "rules"), reason: "project rules" }, | |
| { path: path.join(resolvedProject, ".claude", "keybindings.json"), reason: "project keybindings" }, | |
| { path: path.join(resolvedProject, ".claude", "scheduled_tasks.json"), reason: "project scheduled tasks" }, | |
| { path: path.join(resolvedProject, ".claude", "scheduled_tasks.lock"), reason: "project scheduled tasks lock" }, | |
| { path: path.join(resolvedProject, ".claude", "CLAUDE.md"), reason: "project Claude instructions" }, | |
| { path: path.join(resolvedProject, ".claude", "CLAUDE.local.md"), reason: "project Claude instructions (local)" }, | |
| { path: path.join(resolvedProject, ".mcp.json"), reason: "project MCP configuration" }, | |
| ]; | |
| return includeConfig ? [...safe, ...config] : safe; | |
| } | |
| async function collectTargets() { | |
| const targets: Target[] = []; | |
| if (has("--reset")) { | |
| targets.push( | |
| { path: configDir, reason: "all Claude Code user data and configuration", kind: "reset" }, | |
| { path: path.join(home, ".claude.json"), reason: "legacy Claude Code state file, if present", kind: "reset" }, | |
| { path: path.join(home, ".claude.json.backup"), reason: "legacy Claude Code state backup, if present", kind: "reset" }, | |
| ); | |
| return targets; | |
| } | |
| const includeDebug = has("--include-debug"); | |
| const includeConfig = has("--include-config"); | |
| const includeAuth = has("--include-auth") || has("--include-keychain"); | |
| const scanned = await scanConfigDir(); | |
| for (const entry of scanned) { | |
| if (entry.kind === "safe") targets.push(entry); | |
| else if (entry.kind === "debug" && includeDebug) targets.push(entry); | |
| else if (entry.kind === "config" && includeConfig) targets.push(entry); | |
| else if (entry.kind === "auth" && includeAuth) targets.push(entry); | |
| } | |
| const out = outOfDirPaths(); | |
| if (includeConfig) { | |
| for (const entry of out.config) targets.push({ ...entry, kind: "config" }); | |
| } | |
| if (has("--include-native-cache") || has("--uninstall-native")) { | |
| for (const entry of out.native) targets.push({ ...entry, kind: "native" }); | |
| } | |
| if (has("--uninstall-native")) { | |
| for (const entry of out.nativeBinary) targets.push({ ...entry, kind: "native" }); | |
| } | |
| const projectArg = valueOf("--project"); | |
| if (projectArg) { | |
| const resolvedProject = path.resolve(projectArg.replace(/^~/, home)); | |
| for (const entry of projectPaths(resolvedProject, includeConfig)) { | |
| targets.push({ ...entry, kind: "project" }); | |
| } | |
| } | |
| return targets; | |
| } | |
| function listKeychainEntries(): KeychainEntry[] { | |
| if (os.platform() !== "darwin") return []; | |
| const username = process.env.USER || os.userInfo().username || "claude-code-user"; | |
| // Service names per macOsKeychainHelpers.ts: "Claude Code{OAUTH_FILE_SUFFIX}{-credentials?}{-dirHash?}". | |
| // OAUTH_FILE_SUFFIX defaults to "" in production; staging/local/custom builds use "-staging-oauth", "-local-oauth", "-custom-oauth". | |
| // dirHash empty when CLAUDE_CONFIG_DIR is unset; otherwise -<8 hex of sha256(configDir)>. | |
| const oauthSuffixes = ["", "-staging-oauth", "-local-oauth", "-custom-oauth"]; | |
| const credentialSuffixes = ["", "-credentials"]; | |
| let dirHash = ""; | |
| if (process.env.CLAUDE_CONFIG_DIR) { | |
| const crypto = require("node:crypto") as typeof import("node:crypto"); | |
| dirHash = `-${crypto.createHash("sha256").update(configDir).digest("hex").slice(0, 8)}`; | |
| } | |
| const dirHashes = process.env.CLAUDE_CONFIG_DIR ? [dirHash, ""] : [""]; | |
| const candidates: KeychainEntry[] = []; | |
| for (const oauth of oauthSuffixes) { | |
| for (const cred of credentialSuffixes) { | |
| for (const dh of dirHashes) { | |
| candidates.push({ service: `Claude Code${oauth}${cred}${dh}`, account: username }); | |
| } | |
| } | |
| } | |
| const found: KeychainEntry[] = []; | |
| for (const entry of candidates) { | |
| const r = spawnSync("security", ["find-generic-password", "-s", entry.service, "-a", entry.account], { | |
| stdio: ["ignore", "ignore", "ignore"], | |
| }); | |
| if (r.status === 0) found.push(entry); | |
| } | |
| return found; | |
| } | |
| function deleteKeychainEntry(entry: KeychainEntry): boolean { | |
| const r = spawnSync("security", ["delete-generic-password", "-s", entry.service, "-a", entry.account], { | |
| stdio: ["ignore", "ignore", "ignore"], | |
| }); | |
| return r.status === 0; | |
| } | |
| async function main() { | |
| if (has("--help") || has("-h")) { | |
| usage(); | |
| return; | |
| } | |
| const apply = has("--apply"); | |
| const permanentDelete = has("--delete"); | |
| const skipConfirm = has("--yes"); | |
| const includeKeychain = has("--include-keychain"); | |
| if (permanentDelete && !apply) { | |
| throw new Error("--delete requires --apply"); | |
| } | |
| if (includeKeychain && os.platform() !== "darwin") { | |
| console.warn("--include-keychain is macOS-only; ignoring."); | |
| } | |
| const rawTargets = await collectTargets(); | |
| const projectArg = valueOf("--project"); | |
| const resolvedProject = projectArg ? path.resolve(projectArg.replace(/^~/, home)) : undefined; | |
| const allowedRoots = [home, configDir]; | |
| if (resolvedProject) allowedRoots.push(resolvedProject); | |
| const existingTargets: Array<Target & { size: number }> = []; | |
| for (const target of rawTargets) { | |
| const resolved = path.resolve(target.path); | |
| const allowed = allowedRoots.some((root) => pathInside(resolved, root)); | |
| if (!allowed) { | |
| throw new Error(`Refusing to touch path outside allowed roots: ${resolved}`); | |
| } | |
| if (!existsSync(resolved)) continue; | |
| if (await isSymlink(resolved)) { | |
| console.warn(`Skipping symlink: ${resolved}`); | |
| continue; | |
| } | |
| existingTargets.push({ ...target, path: resolved, size: await dirSize(resolved) }); | |
| } | |
| const keychainEntries = includeKeychain && os.platform() === "darwin" ? listKeychainEntries() : []; | |
| console.log(`Claude Code cleanup ${apply ? "apply" : "dry-run"}`); | |
| console.log(`Mode: ${permanentDelete ? "permanent delete" : "move to backup"}`); | |
| console.log(`Config dir: ${configDir}${process.env.CLAUDE_CONFIG_DIR ? " (from $CLAUDE_CONFIG_DIR)" : ""}`); | |
| if (existingTargets.length === 0 && keychainEntries.length === 0) { | |
| console.log("No matching Claude Code data found."); | |
| return; | |
| } | |
| let total = 0; | |
| let unknownCount = 0; | |
| for (const target of existingTargets) { | |
| total += target.size; | |
| if (target.reason === "unknown entry (treated as bloat)") unknownCount++; | |
| console.log(`- ${target.path}`); | |
| console.log(` size: ${formatBytes(target.size)}`); | |
| console.log(` kind: ${target.kind}`); | |
| console.log(` reason: ${target.reason}`); | |
| } | |
| for (const entry of keychainEntries) { | |
| console.log(`- keychain: service=${entry.service} account=${entry.account}`); | |
| console.log(` kind: auth`); | |
| console.log(` reason: macOS Keychain entry (IRREVERSIBLE — will be deleted, not backed up)`); | |
| } | |
| console.log(`Total selected: ${formatBytes(total)}${keychainEntries.length > 0 ? ` + ${keychainEntries.length} keychain entries` : ""}`); | |
| if (unknownCount > 0) { | |
| console.log(`Note: ${unknownCount} entry(ies) classified as "unknown" — review the list above before --apply.`); | |
| } | |
| if (!apply) { | |
| console.log("Dry-run only. Re-run with --apply to clean these paths."); | |
| if (!has("--include-debug") && !has("--reset")) { | |
| console.log("Debug logs/telemetry not selected. Add --include-debug for debug/, statsig/, telemetry/, daemon, IDE state."); | |
| } | |
| if (!has("--include-config") && !has("--reset")) { | |
| console.log("Configuration not selected. Add --include-config to remove settings, extensions, plans, config backups."); | |
| } | |
| if (!has("--include-auth") && !includeKeychain && !has("--reset")) { | |
| console.log("Auth not selected. Add --include-auth for credential files, --include-keychain for macOS Keychain entries."); | |
| } | |
| return; | |
| } | |
| if (!skipConfirm) { | |
| const rl = createInterface({ input, output }); | |
| const expected = permanentDelete ? "DELETE" : "MOVE"; | |
| const answer = await rl.question(`Type ${expected} to continue: `); | |
| if (answer !== expected) { | |
| rl.close(); | |
| console.log("Aborted."); | |
| return; | |
| } | |
| if (keychainEntries.length > 0) { | |
| const kAnswer = await rl.question(`About to permanently remove ${keychainEntries.length} Keychain entries (irreversible). Type KEYCHAIN to confirm: `); | |
| if (kAnswer !== "KEYCHAIN") { | |
| rl.close(); | |
| console.log("Aborted before any changes."); | |
| return; | |
| } | |
| } | |
| rl.close(); | |
| } | |
| if (!permanentDelete && existingTargets.length > 0) { | |
| await mkdir(backupRoot, { recursive: true }); | |
| } | |
| for (const target of existingTargets) { | |
| if (permanentDelete) { | |
| await rm(target.path, { recursive: true, force: true }); | |
| console.log(`Deleted: ${target.path}`); | |
| } else { | |
| const destination = await moveToBackup(target.path); | |
| console.log(`Moved: ${target.path} -> ${destination}`); | |
| } | |
| } | |
| for (const entry of keychainEntries) { | |
| const ok = deleteKeychainEntry(entry); | |
| console.log(`${ok ? "Removed" : "Failed to remove"} keychain: ${entry.service} (${entry.account})`); | |
| } | |
| if (!permanentDelete && existingTargets.length > 0) { | |
| console.log(`Backup folder: ${backupRoot}`); | |
| console.log("After verifying everything works, delete the backup folder manually if you want to reclaim disk space."); | |
| } | |
| console.log("Done."); | |
| } | |
| main().catch((error) => { | |
| console.error(error instanceof Error ? error.message : error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment