feat: add encrypted instance state snapshots

This commit is contained in:
Dotta committed 2026-08-14 07:18:11 +00:00
1 parent aac6ce82e1
commit f9a60389d6
14 files changed
+496 -1

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import * as p from "@clack/prompts";
import pc from "picocolors";
import { printPaperclipCliBanner } from "../utils/banner.js";
type StateApiOptions = { apiUrl?: string; token?: string; json?: boolean };
async function callStateApi(pathname: string, opts: StateApiOptions, body?: unknown) {
const apiUrl = (opts.apiUrl?.trim() || process.env.PAPERCLIP_API_URL?.trim() || "http://127.0.0.1:3100/api").replace(/\/$/, "");
const token = opts.token?.trim() || process.env.PAPERCLIP_API_KEY?.trim();
const response = await fetch(`${apiUrl}${pathname}`, {
method: "POST",
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify(body ?? {}),
});
const result = await response.json();
if (!response.ok) throw new Error(typeof result?.error === "string" ? result.error : `Request failed (${response.status})`);
return result;
}
export async function stateSnapshotCommand(opts: StateApiOptions) {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclip state snapshot ")));
const result = await callStateApi("/instance/state-snapshots", opts);
if (opts.json) console.log(JSON.stringify(result, null, 2));
else p.outro(pc.green(`Snapshot stored: ${result.objectKey}`));
}
export async function stateRestoreCommand(objectKey: string, opts: StateApiOptions) {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclip state restore ")));
const result = await callStateApi("/instance/state-snapshots/restore", opts, { objectKey });
if (opts.json) console.log(JSON.stringify(result, null, 2));
else p.outro(pc.green(`Snapshot restored: ${objectKey}`));
}
+16
View File
@@ -9,6 +9,7 @@ import { heartbeatRun } from "./commands/heartbeat-run.js";
import { runCommand } from "./commands/run.js";
import { bootstrapCeoInvite } from "./commands/auth-bootstrap-ceo.js";
import { dbBackupCommand } from "./commands/db-backup.js";
import { stateRestoreCommand, stateSnapshotCommand } from "./commands/state.js";
import { registerEnvLabCommands } from "./commands/env-lab.js";
import { registerContextCommands } from "./commands/client/context.js";
import { registerCompanyCommands } from "./commands/client/company.js";
@@ -160,6 +161,21 @@ program
await dbBackupCommand(opts);
});
const state = program.command("state").description("Inspect, snapshot, and restore instance state");
state.command("snapshot")
.description("Create an encrypted instance-state snapshot")
.option("--api-url <url>", "Paperclip API base URL")
.option("--token <token>", "Board/admin bearer token")
.option("--json", "Print result as JSON")
.action(stateSnapshotCommand);
state.command("restore")
.description("Restore an encrypted instance-state snapshot")
.argument("<object-key>", "Storage-provider object key returned by state snapshot")
.option("--api-url <url>", "Paperclip API base URL")
.option("--token <token>", "Board/admin bearer token")
.option("--json", "Print result as JSON")
.action(stateRestoreCommand);
program
.command("allowed-hostname")
.description("Allow a hostname for authenticated/private mode access")