From 7eb9f4ac70a1ed87bec131a0daabde17c8c371d3 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Sat, 7 Mar 2026 13:24:10 +1000 Subject: [PATCH] restore: safe default (no gateway restart) + --restart-gateway flag --- src/cli.ts | 17 ++++++++++ src/restore.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/restore.ts diff --git a/src/cli.ts b/src/cli.ts index df8af65..c1b3bc2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { join } from 'node:path'; +import { restoreApply, restoreGuide } from './restore.js'; type Json = any; @@ -223,6 +224,8 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') { console.log(' clawkeeper switch openai|ollama'); console.log(' clawkeeper backup-plan [--json]'); console.log(' clawkeeper verify-backup '); + console.log(' clawkeeper restore-guide'); + console.log(' clawkeeper restore-apply '); process.exit(0); } @@ -257,6 +260,20 @@ else if (cmd === 'verify-backup') { die(`Failed to read tarball: ${e?.message || e}`); } } +else if (cmd === 'restore-guide') { + restoreGuide({ home: HOME }); +} +else if (cmd === 'restore-apply') { + const file = args[0]; + if (!file) die('Usage: clawkeeper restore-apply [--restart-gateway]'); + const restartGateway = args.includes('--restart-gateway'); + const tgz = untildify(file); + try { + restoreApply({ home: HOME, tgzPath: tgz, restartGateway }); + } catch (e: any) { + die(String(e?.message || e)); + } +} else if (cmd === 'switch') { const to = args[0]; if (to !== 'openai' && to !== 'ollama') die('Usage: clawkeeper switch openai|ollama'); diff --git a/src/restore.ts b/src/restore.ts new file mode 100644 index 0000000..2e06ecb --- /dev/null +++ b/src/restore.ts @@ -0,0 +1,89 @@ +import { existsSync, renameSync, mkdirSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { join } from 'node:path'; + +export function untildify(home: string, p: string): string { + if (p.startsWith('~/')) return join(home, p.slice(2)); + return p; +} + +export function run(cmd: string, args: string[], quiet = false): string { + const out = execFileSync(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + const s = out.toString('utf8'); + if (!quiet) process.stdout.write(s); + return s; +} + +export function restoreGuide(opts: { home: string }) { + const { home } = opts; + const lines: string[] = []; + lines.push('Clawkeeper restore guide (safe mode)'); + lines.push(''); + lines.push('0) Install OpenClaw on the new machine'); + lines.push(' - Follow OpenClaw install docs for your OS'); + lines.push(''); + lines.push('1) Copy backup tarball to the new machine, e.g. ~/openclaw-backup.tgz'); + lines.push(''); + lines.push('2) Restore (safe): rename existing ~/.openclaw before extracting'); + lines.push(' clawkeeper restore-apply ~/openclaw-backup.tgz'); + lines.push(''); + lines.push('3) Verify'); + lines.push(' openclaw config validate'); + lines.push(' openclaw gateway restart'); + lines.push(' openclaw status'); + lines.push(' openclaw memory status'); + lines.push(''); + lines.push('Notes:'); + lines.push('- This uses safe mode: if ~/.openclaw exists, it will be renamed to ~/.openclaw.bak.'); + lines.push('- Secrets are restored from ~/.openclaw/credentials and ~/.openclaw/.env (if present in backup).'); + console.log(lines.join('\n')); +} + +export function restoreApply(opts: { home: string; tgzPath: string; restartGateway?: boolean }) { + const { home, tgzPath, restartGateway = false } = opts; + const src = tgzPath; + if (!existsSync(src)) throw new Error(`Backup not found: ${src}`); + + const oc = join(home, '.openclaw'); + if (existsSync(oc)) { + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + const bak = join(home, `.openclaw.bak.${ts}`); + renameSync(oc, bak); + console.log(`Renamed existing ~/.openclaw -> ${bak}`); + } + + // Extract into HOME. The tarball is expected to contain a top-level .openclaw/ folder. + mkdirSync(oc, { recursive: true }); + run('/usr/bin/tar', ['-xzf', src, '-C', home], true); + console.log('Extracted backup into HOME.'); + + // Best-effort validate + try { + run('openclaw', ['config', 'validate'], true); + console.log('openclaw config validate: OK'); + } catch (e: any) { + console.log(`openclaw config validate: WARN (${e?.message || e})`); + } + + if (restartGateway) { + try { + run('openclaw', ['gateway', 'restart'], true); + console.log('openclaw gateway restart: OK'); + } catch (e: any) { + console.log(`openclaw gateway restart: WARN (${e?.message || e})`); + console.log('If gateway service is not loaded, try: openclaw gateway install --force'); + } + } else { + console.log('NOTE: gateway was NOT restarted (safe default).'); + console.log('To restart gateway after restore, run: openclaw gateway restart'); + console.log('If restart fails, run: openclaw gateway install --force'); + } + + try { + const s = run('openclaw', ['status'], true); + console.log('openclaw status: OK'); + console.log(s.split('\n').slice(0, 20).join('\n')); + } catch { + // ignore + } +}