resetComputer.lua
204 lines · 4.9 KB
resetComputer.lua
Copy & run
wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/resetComputer/resetComputer.lua
| 1 | -- resetComputer.lua |
| 2 | -- Safely reset a ComputerCraft computer/turtle to factory defaults by wiping the local HDD. |
| 3 | -- Preserves ROM and any mounted disks. Supports confirmation, dry-run, and keep-list. |
| 4 | |
| 5 | local function printUsage() |
| 6 | print("Usage: resetComputer [options]") |
| 7 | print("") |
| 8 | print("Options:") |
| 9 | print(" --yes, -y Proceed without interactive confirmation") |
| 10 | print(" --dry-run Show what would be deleted, but do not delete") |
| 11 | print(" --keep <path> Preserve a file/dir (can be used multiple times)") |
| 12 | print(" --clear-label Clear the computer label after reset") |
| 13 | print(" --no-reboot Do not reboot automatically after reset") |
| 14 | print(" --help, -h Show this help") |
| 15 | end |
| 16 | |
| 17 | local function normalizePath(path) |
| 18 | if not path or path == "" then return "/" end |
| 19 | if string.sub(path, 1, 1) ~= "/" then |
| 20 | return "/" .. path |
| 21 | end |
| 22 | return path |
| 23 | end |
| 24 | |
| 25 | local function parseArgs(tArgs) |
| 26 | local options = { |
| 27 | assumeYes = false, |
| 28 | dryRun = false, |
| 29 | clearLabel = false, |
| 30 | noReboot = false, |
| 31 | keeps = {}, |
| 32 | help = false, |
| 33 | } |
| 34 | |
| 35 | local i = 1 |
| 36 | while i <= #tArgs do |
| 37 | local a = tArgs[i] |
| 38 | if a == "--yes" or a == "-y" then |
| 39 | options.assumeYes = true |
| 40 | elseif a == "--dry-run" then |
| 41 | options.dryRun = true |
| 42 | elseif a == "--clear-label" then |
| 43 | options.clearLabel = true |
| 44 | elseif a == "--no-reboot" then |
| 45 | options.noReboot = true |
| 46 | elseif a == "--help" or a == "-h" then |
| 47 | options.help = true |
| 48 | elseif a == "--keep" then |
| 49 | if i == #tArgs then |
| 50 | print("Error: --keep requires a path argument") |
| 51 | options.help = true |
| 52 | break |
| 53 | end |
| 54 | local keepPath = normalizePath(tArgs[i + 1]) |
| 55 | options.keeps[keepPath] = true |
| 56 | i = i + 1 |
| 57 | else |
| 58 | -- Unrecognized positional may be treated as keep for convenience |
| 59 | local keepPath = normalizePath(a) |
| 60 | options.keeps[keepPath] = true |
| 61 | end |
| 62 | i = i + 1 |
| 63 | end |
| 64 | |
| 65 | return options |
| 66 | end |
| 67 | |
| 68 | local function isKept(path, keeps) |
| 69 | if keeps[path] then return true end |
| 70 | -- Also keep if any ancestor is marked keep |
| 71 | -- and if any kept path is a child of this path, we skip deleting parent entirely |
| 72 | for keepPath, _ in pairs(keeps) do |
| 73 | if string.sub(path, 1, #keepPath) == keepPath then |
| 74 | return true |
| 75 | end |
| 76 | if string.sub(keepPath, 1, #path) == path and (string.len(keepPath) > string.len(path)) then |
| 77 | return true |
| 78 | end |
| 79 | end |
| 80 | return false |
| 81 | end |
| 82 | |
| 83 | local function listTopLevelDeletions(keeps) |
| 84 | local toDelete = {} |
| 85 | for _, name in ipairs(fs.list("/")) do |
| 86 | local full = "/" .. name |
| 87 | local drive = fs.getDrive(full) |
| 88 | if drive == "hdd" then |
| 89 | if not isKept(full, keeps) then |
| 90 | table.insert(toDelete, full) |
| 91 | end |
| 92 | else |
| 93 | -- Skip ROM and mounted disks |
| 94 | end |
| 95 | end |
| 96 | table.sort(toDelete) |
| 97 | return toDelete |
| 98 | end |
| 99 | |
| 100 | local function deletePath(path) |
| 101 | if fs.isReadOnly(path) then return false, "read-only" end |
| 102 | if not fs.exists(path) then return true end |
| 103 | local ok, err = pcall(function() |
| 104 | fs.delete(path) |
| 105 | end) |
| 106 | if not ok then return false, err end |
| 107 | return true |
| 108 | end |
| 109 | |
| 110 | local function confirmPrompt(summary) |
| 111 | print("") |
| 112 | print(summary) |
| 113 | print("Type 'RESET' to confirm, or anything else to cancel.") |
| 114 | write("> ") |
| 115 | local input = read() |
| 116 | return input == "RESET" |
| 117 | end |
| 118 | |
| 119 | -- capture program arguments at the top-level chunk (varargs allowed here) |
| 120 | local topArgs = {...} |
| 121 | |
| 122 | local function main(tArgs) |
| 123 | local opts = parseArgs(tArgs) |
| 124 | if opts.help then |
| 125 | printUsage() |
| 126 | return |
| 127 | end |
| 128 | |
| 129 | term.clear() |
| 130 | term.setCursorPos(1, 1) |
| 131 | print("Reset Computer / Turtle") |
| 132 | print("This will wipe the local HDD (excluding ROM and mounted disks).") |
| 133 | |
| 134 | local toDelete = listTopLevelDeletions(opts.keeps) |
| 135 | if #toDelete == 0 then |
| 136 | print("Nothing to delete (local HDD already clean or everything kept).") |
| 137 | return |
| 138 | end |
| 139 | |
| 140 | print("") |
| 141 | print("Targets:") |
| 142 | for _, p in ipairs(toDelete) do |
| 143 | print(" " .. p) |
| 144 | end |
| 145 | |
| 146 | if opts.dryRun then |
| 147 | print("") |
| 148 | print("Dry-run: No changes made.") |
| 149 | return |
| 150 | end |
| 151 | |
| 152 | local proceed = opts.assumeYes |
| 153 | if not proceed then |
| 154 | proceed = confirmPrompt("About to delete " .. tostring(#toDelete) .. " top-level item(s).") |
| 155 | end |
| 156 | if not proceed then |
| 157 | print("Cancelled.") |
| 158 | return |
| 159 | end |
| 160 | |
| 161 | print("") |
| 162 | print("Deleting...") |
| 163 | local failures = 0 |
| 164 | for _, p in ipairs(toDelete) do |
| 165 | local ok, err = deletePath(p) |
| 166 | if ok then |
| 167 | print("deleted " .. p) |
| 168 | else |
| 169 | print("FAILED " .. p .. " (" .. tostring(err) .. ")") |
| 170 | failures = failures + 1 |
| 171 | end |
| 172 | end |
| 173 | |
| 174 | if opts.clearLabel then |
| 175 | local ok, _ = pcall(function() |
| 176 | if os.setComputerLabel then |
| 177 | os.setComputerLabel(nil) |
| 178 | end |
| 179 | end) |
| 180 | if ok then |
| 181 | print("Cleared computer label.") |
| 182 | end |
| 183 | end |
| 184 | |
| 185 | print("") |
| 186 | if failures == 0 then |
| 187 | print("Reset complete.") |
| 188 | else |
| 189 | print("Reset complete with " .. tostring(failures) .. " failure(s).") |
| 190 | end |
| 191 | |
| 192 | if not opts.noReboot then |
| 193 | print("Rebooting in 2 seconds...") |
| 194 | sleep(2) |
| 195 | os.reboot() |
| 196 | else |
| 197 | print("Skipping reboot as requested.") |
| 198 | end |
| 199 | end |
| 200 | |
| 201 | main(topArgs) |
| 202 | |
| 203 | |
| 204 |