Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions packages/cli/bin/tps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const cli = meow(
review <name> Performance review for a specific agent
office <action> Branch office sandbox lifecycle (start/stop/list/status/kill)
bootstrap <agent-id> Bring a hired agent to operational state
backup <agent-id> [--schedule daily|off] [--keep n] [--sanitize] Backup agent workspace
backup Archive critical TPS host files
restore <agent-id> <archive> [--clone] [--overwrite] [--from <archive>] Restore agent workspace from a backup
status [agent-id] [--auto-prune] [--prune] [--json] [--cost] [--shared]
heartbeat <agent-id> [--nonono] Send a heartbeat/ping for an agent
Expand Down Expand Up @@ -55,7 +55,7 @@ const cli = meow(
$ tps office start branch-a
$ tps office status branch-a
$ tps bootstrap flint
$ tps backup flint --schedule daily
$ tps backup
$ tps restore flint ~/.tps/backups/flint/old.tps-backup.tar.gz
$ tps status
$ tps status flint --cost
Expand Down Expand Up @@ -677,19 +677,14 @@ async function main() {
break;
}
case "backup": {
const agentId = rest[0];
if (!agentId) {
console.error("Usage: tps backup <agent-id> [--schedule daily|off] [--keep n]");
process.exit(1);
const subCmd = rest[0];
if (subCmd === "keys") {
const { runBackupSecrets } = await import("../src/commands/backup.js");
await runBackupSecrets();
} else {
const { runBackup } = await import("../src/commands/backup.js");
await runBackup({ agentId: subCmd });
}
const { runBackup } = await import("../src/commands/backup.js");
await runBackup({
agentId,
keep: typeof cli.flags.keep === "number" ? Number(cli.flags.keep) : undefined,
schedule: cli.flags.schedule,
sanitize: cli.flags.sanitize,
configPath: cli.flags.config,
});
break;
}
case "restore": {
Expand Down
47 changes: 41 additions & 6 deletions packages/cli/src/commands/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
chmodSync,
copyFileSync,
existsSync,
globSync,
lstatSync,
mkdirSync,
readdirSync,
Expand All @@ -13,9 +14,9 @@ import {
writeFileSync,
statSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { homedir, tmpdir } from "node:os";
import { join, dirname, resolve, sep } from "node:path";
import { spawnSync } from "node:child_process";
import { execSync, spawnSync } from "node:child_process";
import { readOpenClawConfig, resolveConfigPath, getAgentList, type OpenClawConfig, type OpenClawAgent } from "../utils/config.js";
import { sanitizeIdentifier } from "../schema/sanitizer.js";
import { workspacePath, resolveTeamId } from "../utils/workspace.js";
Expand Down Expand Up @@ -44,7 +45,7 @@ interface Manifest {
}

export interface BackupArgs {
agentId: string;
agentId?: string;
keep?: number;
schedule?: string;
from?: string;
Expand Down Expand Up @@ -489,6 +490,7 @@ function backupFilesWithManifest(workspace: string, entry: OpenClawAgent | null,
}

export async function runBackup(args: BackupArgs): Promise<void> {
if (!args.agentId) throw new Error("Usage: tps backup <agent-id>");
const safeId = sanitizeAgentId(args.agentId);
const workspace = workspacePath(safeId);

Expand Down Expand Up @@ -598,14 +600,47 @@ export async function runBackup(args: BackupArgs): Promise<void> {
}
}

console.log(`✅ Backup complete: ${archivePath}`);
console.log(`📦 Files: ${manifest.files.length}`);
console.log(`🔐 Host: ${manifest.sourceHostFingerprint}`);
console.log(`Backup complete: ${archivePath}`);
} finally {
rmSync(stagingDir, { recursive: true, force: true });
}
}

export async function runBackupSecrets(): Promise<void> {
const home = homedir();
const backupDir = join(home, ".tps", "backups");
mkdirSync(backupDir, { recursive: true });

const archivePath = join(backupDir, `backup-${new Date().toISOString().slice(0, 10)}.tar.gz`);
const includedFiles = Array.from(new Set([
...globSync(".tps/identity/*.key", { cwd: home }),
...globSync(".tps/secrets/**/*", { cwd: home }),
...globSync(".tps/agents/*/agent.yaml", { cwd: home }),
...globSync(".codex/auth.json", { cwd: home }),
])).filter((relativePath) => {
const absolutePath = join(home, relativePath);
return existsSync(absolutePath) && lstatSync(absolutePath).isFile();
}).sort();

if (includedFiles.length === 0) {
console.log("No TPS backup files found.");
return;
}

const shellQuote = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`;
const tarArgs = includedFiles.map(shellQuote).join(" ");
execSync(`tar czf ${shellQuote(archivePath)} -C ${shellQuote(home)} ${tarArgs}`, {
stdio: "pipe",
});
chmodSync(archivePath, 0o600);

for (const relativePath of includedFiles) {
console.log(relativePath);
}

console.log(`Archive: ${archivePath} (${statSync(archivePath).size} bytes)`);
}

export async function runRestore(args: RestoreArgs): Promise<void> {
const safeTarget = sanitizeAgentId(args.agentId);
const archive = resolve(args.archivePath);
Expand Down
Loading