feat(editor): add --remove-zone for safe zone deletion

Counterpart to --scaffold-zone / --copy-zone / --rename-zone — completes
the zone-lifecycle CRUD. Defense-in-depth against accidental
destruction:

  wowee_editor --remove-zone custom_zones/Doomed

  remove-zone: custom_zones/Doomed ('Doomed')
    would delete: 6 file(s), 174.9 KB
    re-run with --confirm to actually delete

  wowee_editor --remove-zone custom_zones/Doomed --confirm

  Removed custom_zones/Doomed ('Doomed')
    deleted: 7 filesystem entries, 174.9 KB freed

Two-step safety:
1. Without --confirm: dry-run that lists what would be deleted
   (file count + total bytes + zone display name from manifest).
2. With --confirm: actually wipes the directory.

Belt-and-suspenders refusal: even with --confirm, refuses to delete
anything that doesn't have a zone.json at the top level. Catches
typos like '--remove-zone .' that would otherwise nuke an entire
project.

Why not just 'rm -rf'? --remove-zone gives:
- Per-zone display name in the confirmation
- Byte-count audit before deletion
- The non-zone-dir guard (rm doesn't know what a zone is)
- Symmetric with the rest of the zone-lifecycle CLI

Verified: dry-run lists 6 files / 175 KB; '. --confirm' correctly
refused (no zone.json at top level); zone-dir --confirm wiped 7
fs entries with byte tally.
This commit is contained in:
Kelsi 2026-05-06 19:26:10 -07:00
parent bc9033eb43
commit 8d9690a57a

View file

@ -571,6 +571,8 @@ static void printUsage(const char* argv0) {
std::printf(" Duplicate a zone to custom_zones/<slug>/ with renamed slug-prefixed files\n");
std::printf(" --rename-zone <srcDir> <newName>\n");
std::printf(" In-place rename (zone.json + slug-prefixed files + dir); no copy\n");
std::printf(" --remove-zone <zoneDir> [--confirm]\n");
std::printf(" Delete a zone directory entirely (requires --confirm to actually delete)\n");
std::printf(" --clear-zone-content <zoneDir> [--creatures] [--objects] [--quests] [--all]\n");
std::printf(" Wipe one or more content files (terrain + manifest preserved)\n");
std::printf(" --strip-zone <zoneDir> [--dry-run]\n");
@ -857,7 +859,8 @@ int main(int argc, char* argv[]) {
"--remove-quest-objective", "--clone-quest", "--clone-creature",
"--clone-object",
"--remove-creature", "--remove-object", "--remove-quest",
"--copy-zone", "--rename-zone", "--clear-zone-content", "--strip-zone",
"--copy-zone", "--rename-zone", "--remove-zone",
"--clear-zone-content", "--strip-zone",
"--repair-zone", "--gen-makefile", "--gen-project-makefile",
"--build-woc", "--regen-collision", "--fix-zone",
"--export-png", "--export-obj", "--import-obj",
@ -11786,6 +11789,66 @@ int main(int argc, char* argv[]) {
std::printf(" mapName : %s -> %s\n", oldSlug.c_str(), newSlug.c_str());
std::printf(" renamed : %d slug-prefixed file(s)\n", renamed);
return 0;
} else if (std::strcmp(argv[i], "--remove-zone") == 0 && i + 1 < argc) {
// Delete a zone directory entirely. Requires --confirm to
// actually delete (defense against accidental destruction
// and against shell glob mishaps). Without --confirm,
// just lists what would be deleted.
std::string zoneDir = argv[++i];
bool confirm = false;
if (i + 1 < argc && std::strcmp(argv[i + 1], "--confirm") == 0) {
confirm = true; i++;
}
namespace fs = std::filesystem;
if (!fs::exists(zoneDir)) {
std::fprintf(stderr,
"remove-zone: %s does not exist\n", zoneDir.c_str());
return 1;
}
if (!fs::exists(zoneDir + "/zone.json")) {
// Belt-and-suspenders: refuse to wipe anything that doesn't
// look like a zone dir, even with --confirm. Catches typos
// like '--remove-zone .' that would nuke the whole project.
std::fprintf(stderr,
"remove-zone: %s has no zone.json — refusing to delete (not a zone dir)\n",
zoneDir.c_str());
return 1;
}
// Read manifest for the user-facing name.
wowee::editor::ZoneManifest zm;
std::string zoneName = zoneDir;
if (zm.load(zoneDir + "/zone.json")) {
zoneName = zm.displayName.empty() ? zm.mapName : zm.displayName;
}
// Walk for what would be removed (counts + total bytes).
int fileCount = 0;
uint64_t totalBytes = 0;
std::error_code ec;
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
if (!e.is_regular_file()) continue;
fileCount++;
totalBytes += e.file_size(ec);
}
if (!confirm) {
std::printf("remove-zone: %s ('%s')\n",
zoneDir.c_str(), zoneName.c_str());
std::printf(" would delete: %d file(s), %.1f KB\n",
fileCount, totalBytes / 1024.0);
std::printf(" re-run with --confirm to actually delete\n");
return 0;
}
// Confirmed — wipe it.
uintmax_t removed = fs::remove_all(zoneDir, ec);
if (ec) {
std::fprintf(stderr,
"remove-zone: failed to remove %s (%s)\n",
zoneDir.c_str(), ec.message().c_str());
return 1;
}
std::printf("Removed %s ('%s')\n", zoneDir.c_str(), zoneName.c_str());
std::printf(" deleted: %ju filesystem entries, %.1f KB freed\n",
static_cast<uintmax_t>(removed), totalBytes / 1024.0);
return 0;
} else if (std::strcmp(argv[i], "--clear-zone-content") == 0 && i + 1 < argc) {
// Wipe content files (creatures.json / objects.json /
// quests.json) from a zone while keeping terrain + manifest