File size: 3,458 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/** Local recovery retirement. This handler never invokes update or Doctor repair. */
import { confirm, isCancel } from "@clack/prompts";
import {
  inspectSessionSqliteRecovery,
  type RecoveryCleanupReport,
} from "../../commands/doctor-session-sqlite-recovery-inventory.js";
import { retireSessionSqliteRecovery } from "../../commands/doctor-session-sqlite-retirement.js";
import { readSourceConfigBestEffort } from "../../config/io.js";
import { defaultRuntime, writeRuntimeJson } from "../../runtime.js";

function renderCleanup(report: RecoveryCleanupReport): void {
  defaultRuntime.log(`Recovery cleanup: ${report.stateDir}`);
  for (const item of report.artifacts) {
    defaultRuntime.log(
      `  ${item.outcome}: ${item.path} (${item.bytes} bytes; ${item.reason})${item.detail ? ` ${item.detail}` : ""}`,
    );
  }
  defaultRuntime.log(
    `Candidates: ${report.totals.candidateBytes} bytes; verification required: ${report.totals.verificationRequiredBytes}; protected: ${report.totals.protectedBytes}; blocked: ${report.totals.blockedBytes}.`,
  );
  defaultRuntime.log(
    "Retiring originals permanently loses rollback, including pre-repair branches and metadata. Logical bytes are not a promise of physical space reclaimed.",
  );
  if (report.artifacts.length === 0) {
    defaultRuntime.log("No recorded recovery artifacts found.");
  }
  if (report.status === "complete") {
    defaultRuntime.log(
      `Removed ${report.totals.removedFiles} files (${report.totals.removedBytes} logical bytes).`,
    );
  }
}

export async function updateCleanupCommand(options: {
  dryRun?: boolean;
  json?: boolean;
  yes?: boolean;
}): Promise<void> {
  let report: RecoveryCleanupReport | undefined;
  try {
    const readConfig = () => readSourceConfigBestEffort();
    report = inspectSessionSqliteRecovery({ cfg: await readConfig(), env: process.env });
    if (!options.dryRun) {
      if (report.artifacts.length === 0 && !options.yes) {
        report.status = "complete";
      } else if (!options.yes && (options.json || !process.stdin.isTTY || !process.stderr.isTTY)) {
        report.status = "refused";
      } else {
        report = await retireSessionSqliteRecovery({
          env: process.env,
          preview: report,
          readConfig,
          confirm: async (verified) => {
            if (options.yes || verified.artifacts.length === 0) {
              return true;
            }
            renderCleanup(verified);
            const answer = await confirm({
              message: "Permanently retire these rollback originals?",
              initialValue: false,
              output: process.stderr,
            });
            return !isCancel(answer) && answer;
          },
        });
      }
    }
    if (options.json) {
      writeRuntimeJson(defaultRuntime, { ...report, dryRun: options.dryRun === true });
    } else {
      renderCleanup(report);
    }
    if (report.status === "refused") {
      defaultRuntime.error(
        "Nothing removed. Review with `openclaw update cleanup --dry-run`; use --yes to acknowledge permanent rollback loss.",
      );
      defaultRuntime.exit(1);
    } else if (report.status === "blocked") {
      defaultRuntime.exit(1);
    }
  } catch (error) {
    if (options.json) {
      writeRuntimeJson(defaultRuntime, { ...report, status: "blocked", error: String(error) });
    } else {
      defaultRuntime.error(String(error));
    }
    defaultRuntime.exit(1);
  }
}