Export a workspace without needing a filesystem (#685)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-15 14:29:37 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent a8c845b091
commit 77b18d3477
13 changed files with 87 additions and 56 deletions
@@ -65,21 +65,22 @@ function ExportDataDialogContent({
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]); const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined; const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces"; const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces";
const exportPath = await platform.dialog.save({ const document = await rpc<string>("cmd_export_data", {
title: "Export Data",
defaultPath: `yaak.${slug}.json`,
});
if (exportPath == null) {
return;
}
await rpc("cmd_export_data", {
workspaceIds: ids, workspaceIds: ids,
exportPath,
includePrivateEnvironments: includePrivateEnvironments, includePrivateEnvironments: includePrivateEnvironments,
}); });
const savedTo = await platform.files.save(
`yaak.${slug}.json`,
new TextEncoder().encode(document),
[{ name: "JSON", extensions: ["json"] }],
);
if (savedTo == null) {
return; // Cancelled
}
onHide(); onHide();
onSuccess(exportPath); onSuccess(savedTo);
}, [includePrivateEnvironments, onHide, onSuccess, selectedWorkspaces, workspaces]); }, [includePrivateEnvironments, onHide, onSuccess, selectedWorkspaces, workspaces]);
const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]); const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]);
@@ -130,14 +130,15 @@ fn format_skipped(items: &[ImportPlanItem]) -> Option<String> {
fn export(ctx: &CliContext, args: ExportArgs) -> CommandResult<usize> { fn export(ctx: &CliContext, args: ExportArgs) -> CommandResult<usize> {
let workspace_ids = resolve_export_workspace_ids(ctx, args.workspace_ids, args.all)?; let workspace_ids = resolve_export_workspace_ids(ctx, args.workspace_ids, args.all)?;
let workspace_id_refs: Vec<&str> = workspace_ids.iter().map(String::as_str).collect(); let workspace_id_refs: Vec<&str> = workspace_ids.iter().map(String::as_str).collect();
export::export_data(ExportDataParams { let document = export::export_data(ExportDataParams {
query_manager: ctx.query_manager(), query_manager: ctx.query_manager(),
yaak_version: env!("CARGO_PKG_VERSION"), yaak_version: env!("CARGO_PKG_VERSION"),
export_path: &args.file,
workspace_ids: workspace_id_refs, workspace_ids: workspace_id_refs,
include_private_environments: args.include_private_environments, include_private_environments: args.include_private_environments,
}) })
.map_err(|e| format!("Failed to export data: {e}"))?; .map_err(|e| format!("Failed to export data: {e}"))?;
std::fs::write(&args.file, document)
.map_err(|e| format!("Failed to write {}: {e}", args.file.display()))?;
Ok(workspace_ids.len()) Ok(workspace_ids.len())
} }
+1 -1
View File
@@ -667,7 +667,7 @@ async fn cmd_curl_to_request<R: Runtime>(
Ok(yaak_commands::actions::cmd_curl_to_request(ctx, req).await?) Ok(yaak_commands::actions::cmd_curl_to_request(ctx, req).await?)
} }
async fn cmd_export_data<R: Runtime>(ctx: ClientCtx<R>, req: CmdExportDataReq) -> Result<()> { async fn cmd_export_data<R: Runtime>(ctx: ClientCtx<R>, req: CmdExportDataReq) -> Result<String> {
Ok(yaak_commands::data::cmd_export_data(ctx, req).await?) Ok(yaak_commands::data::cmd_export_data(ctx, req).await?)
} }
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -374,7 +374,6 @@ pub struct CmdCreateExampleWorkspaceReq {}
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_rpc.ts")] #[ts(export, export_to = "gen_rpc.ts")]
pub struct CmdExportDataReq { pub struct CmdExportDataReq {
pub export_path: String,
pub workspace_ids: Vec<String>, pub workspace_ids: Vec<String>,
pub include_private_environments: bool, pub include_private_environments: bool,
} }
@@ -960,7 +959,7 @@ macro_rules! with_commands {
cmd_call_grpc_request_action(CmdCallGrpcRequestActionReq) -> (), cmd_call_grpc_request_action(CmdCallGrpcRequestActionReq) -> (),
cmd_call_http_authentication_action(CmdCallHttpAuthenticationActionReq) -> (), cmd_call_http_authentication_action(CmdCallHttpAuthenticationActionReq) -> (),
cmd_curl_to_request(CmdCurlToRequestReq) -> HttpRequest, cmd_curl_to_request(CmdCurlToRequestReq) -> HttpRequest,
cmd_export_data(CmdExportDataReq) -> (), cmd_export_data(CmdExportDataReq) -> String,
cmd_create_example_workspace(CmdCreateExampleWorkspaceReq) -> BatchUpsertResult, cmd_create_example_workspace(CmdCreateExampleWorkspaceReq) -> BatchUpsertResult,
cmd_save_base64_to_binary(CmdSaveBase64ToBinaryReq) -> (), cmd_save_base64_to_binary(CmdSaveBase64ToBinaryReq) -> (),
cmd_save_response(CmdSaveResponseReq) -> (), cmd_save_response(CmdSaveResponseReq) -> (),
+1 -3
View File
@@ -2,19 +2,17 @@
use crate::error::Result; use crate::error::Result;
use crate::host::Host; use crate::host::Host;
use std::path::Path;
use yaak::example::create_example_workspace; use yaak::example::create_example_workspace;
use yaak::export::{self, ExportDataParams}; use yaak::export::{self, ExportDataParams};
use yaak_models::util::BatchUpsertResult; use yaak_models::util::BatchUpsertResult;
use yaak_rpc_schema::*; use yaak_rpc_schema::*;
use yaak_templates::format_json::format_json; use yaak_templates::format_json::format_json;
pub async fn cmd_export_data<H: Host>(host: H, req: CmdExportDataReq) -> Result<()> { pub async fn cmd_export_data<H: Host>(host: H, req: CmdExportDataReq) -> Result<String> {
let version = host.app_version(); let version = host.app_version();
Ok(export::export_data(ExportDataParams { Ok(export::export_data(ExportDataParams {
query_manager: host.query_manager(), query_manager: host.query_manager(),
yaak_version: &version, yaak_version: &version,
export_path: Path::new(&req.export_path),
workspace_ids: req.workspace_ids.iter().map(|s| s.as_str()).collect(), workspace_ids: req.workspace_ids.iter().map(|s| s.as_str()).collect(),
include_private_environments: req.include_private_environments, include_private_environments: req.include_private_environments,
})?) })?)
+1 -1
View File
@@ -14,4 +14,4 @@
"./yaak_wasm.js", "./yaak_wasm.js",
"./snippets/*" "./snippets/*"
] ]
} }
+19 -19
View File
@@ -512,7 +512,7 @@ export function __wbg_new_typed_c072c4ce9a2a0cdf(arg0, arg1) {
const a = state0.a; const a = state0.a;
state0.a = 0; state0.a = 0;
try { try {
return wasm_bindgen__convert__closures_____invoke__h2cf3f4cce3b29948(a, state0.b, arg0, arg1); return wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined_______true_(a, state0.b, arg0, arg1);
} finally { } finally {
state0.a = a; state0.a = a;
} }
@@ -697,23 +697,23 @@ export function __wbg_warn_b6f36cac66fc96a4(arg0, arg1) {
console.warn(arg0, arg1); console.warn(arg0, arg1);
} }
export function __wbindgen_cast_0000000000000001(arg0, arg1) { export function __wbindgen_cast_0000000000000001(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1123, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1115, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3); const ret = makeMutClosure(arg0, arg1, wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___wasm_bindgen_9b5275515258a0f8___JsValue__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsError___true_);
return ret; return ret;
} }
export function __wbindgen_cast_0000000000000002(arg0, arg1) { export function __wbindgen_cast_0000000000000002(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 214, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 207, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4); const ret = makeMutClosure(arg0, arg1, wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_Event__Event______true_);
return ret; return ret;
} }
export function __wbindgen_cast_0000000000000003(arg0, arg1) { export function __wbindgen_cast_0000000000000003(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 164, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 172, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf); const ret = makeMutClosure(arg0, arg1, wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_IdbVersionChangeEvent__IdbVersionChangeEvent__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsValue___true_);
return ret; return ret;
} }
export function __wbindgen_cast_0000000000000004(arg0, arg1) { export function __wbindgen_cast_0000000000000004(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 212, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f); const ret = makeMutClosure(arg0, arg1, wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke_______true_);
return ret; return ret;
} }
export function __wbindgen_cast_0000000000000005(arg0) { export function __wbindgen_cast_0000000000000005(arg0) {
@@ -750,30 +750,30 @@ export function __wbindgen_init_externref_table() {
table.set(offset + 2, true); table.set(offset + 2, true);
table.set(offset + 3, false); table.set(offset + 3, false);
} }
function wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f(arg0, arg1) { function wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke_______true_(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f(arg0, arg1); wasm.wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke_______true_(arg0, arg1);
} }
function wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4(arg0, arg1, arg2) { function wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_Event__Event______true_(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4(arg0, arg1, arg2); wasm.wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_Event__Event______true_(arg0, arg1, arg2);
} }
function wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3(arg0, arg1, arg2) { function wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___wasm_bindgen_9b5275515258a0f8___JsValue__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsError___true_(arg0, arg1, arg2) {
const ret = wasm.wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3(arg0, arg1, arg2); const ret = wasm.wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___wasm_bindgen_9b5275515258a0f8___JsValue__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsError___true_(arg0, arg1, arg2);
if (ret[1]) { if (ret[1]) {
throw takeFromExternrefTable0(ret[0]); throw takeFromExternrefTable0(ret[0]);
} }
} }
function wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf(arg0, arg1, arg2) { function wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_IdbVersionChangeEvent__IdbVersionChangeEvent__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsValue___true_(arg0, arg1, arg2) {
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf(arg0, arg1, arg2); const ret = wasm.wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_IdbVersionChangeEvent__IdbVersionChangeEvent__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsValue___true_(arg0, arg1, arg2);
if (ret[1]) { if (ret[1]) {
throw takeFromExternrefTable0(ret[0]); throw takeFromExternrefTable0(ret[0]);
} }
} }
function wasm_bindgen__convert__closures_____invoke__h2cf3f4cce3b29948(arg0, arg1, arg2, arg3) { function wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined_______true_(arg0, arg1, arg2, arg3) {
wasm.wasm_bindgen__convert__closures_____invoke__h2cf3f4cce3b29948(arg0, arg1, arg2, arg3); wasm.wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined_______true_(arg0, arg1, arg2, arg3);
} }
Binary file not shown.
+5 -5
View File
@@ -17,11 +17,11 @@ export const rust_sqlite_wasm_malloc: (a: number) => number;
export const rust_sqlite_wasm_realloc: (a: number, b: number) => number; export const rust_sqlite_wasm_realloc: (a: number, b: number) => number;
export const sqlite3_os_end: () => number; export const sqlite3_os_end: () => number;
export const sqlite3_os_init: () => number; export const sqlite3_os_init: () => number;
export const wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3: (a: number, b: number, c: any) => [number, number]; export const wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___wasm_bindgen_9b5275515258a0f8___JsValue__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsError___true_: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf: (a: number, b: number, c: any) => [number, number]; export const wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_IdbVersionChangeEvent__IdbVersionChangeEvent__core_f0fd674eaa06beef___result__Result_____wasm_bindgen_9b5275515258a0f8___JsValue___true_: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h2cf3f4cce3b29948: (a: number, b: number, c: any, d: any) => void; export const wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined___js_sys_434c4d6d72aa7d1b___Function_fn_wasm_bindgen_9b5275515258a0f8___JsValue_____wasm_bindgen_9b5275515258a0f8___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4: (a: number, b: number, c: any) => void; export const wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke___web_sys_e1a11cd1518a8b4d___features__gen_Event__Event______true_: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f: (a: number, b: number) => void; export const wasm_bindgen_9b5275515258a0f8___convert__closures_____invoke_______true_: (a: number, b: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void; export const __wbindgen_exn_store: (a: number) => void;
+28
View File
@@ -46,6 +46,11 @@ const DB_NAME: &str = "yaak.db";
const BLOB_DB_NAME: &str = "yaak-blobs.db"; const BLOB_DB_NAME: &str = "yaak-blobs.db";
const VFS_NAME: &str = "yaak-idb"; const VFS_NAME: &str = "yaak-idb";
/// What an export made here records as the version that wrote it. The desktop stamps its own
/// app version; this host has none, and the field is provenance rather than something read
/// back, so it says what it is.
const EXPORT_VERSION: &str = "web";
struct Host { struct Host {
queries: QueryManager, queries: QueryManager,
blobs: BlobManager, blobs: BlobManager,
@@ -218,6 +223,13 @@ struct UpsertIntrospectionReq {
content: Option<String>, content: Option<String>,
} }
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExportDataReq {
workspace_ids: Vec<String>,
include_private_environments: bool,
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ResponseIdReq { struct ResponseIdReq {
@@ -404,6 +416,22 @@ fn dispatch(
to_json(()) to_json(())
} }
// The export document, built by the same `yaak-models` helper the desktop and the CLI
// build it with. Nothing about what an export *is* is decided here — this host only
// differs in what happens to the bytes afterwards, which is the tab's business.
"cmd_export_data" => {
let req: ExportDataReq = from_js(payload)?;
let db = host.queries.connect();
let export = yaak_models::util::get_workspace_export_resources(
&db,
EXPORT_VERSION,
req.workspace_ids.iter().map(|s| s.as_str()).collect(),
req.include_private_environments,
)
.map_err(js_error)?;
to_json(serde_json::to_string_pretty(&export).map_err(js_error)?)
}
"cmd_get_workspace_meta" => { "cmd_get_workspace_meta" => {
let req: WorkspaceIdReq = from_js(payload)?; let req: WorkspaceIdReq = from_js(payload)?;
let workspace = let workspace =
+7 -9
View File
@@ -1,18 +1,20 @@
use crate::Result; use crate::Result;
use std::fs::File;
use std::path::Path;
use yaak_models::query_manager::QueryManager; use yaak_models::query_manager::QueryManager;
use yaak_models::util::get_workspace_export_resources; use yaak_models::util::get_workspace_export_resources;
pub struct ExportDataParams<'a> { pub struct ExportDataParams<'a> {
pub query_manager: &'a QueryManager, pub query_manager: &'a QueryManager,
pub yaak_version: &'a str, pub yaak_version: &'a str,
pub export_path: &'a Path,
pub workspace_ids: Vec<&'a str>, pub workspace_ids: Vec<&'a str>,
pub include_private_environments: bool, pub include_private_environments: bool,
} }
pub fn export_data(params: ExportDataParams<'_>) -> Result<()> { /// The export document, as JSON.
///
/// Returned rather than written: where an export goes is the host's to decide, and a browser
/// tab has no path to be handed. The desktop hands the bytes to its save dialog; a tab hands
/// them to a download. Neither needs this function to know which.
pub fn export_data(params: ExportDataParams<'_>) -> Result<String> {
let db = params.query_manager.connect(); let db = params.query_manager.connect();
let export_data = get_workspace_export_resources( let export_data = get_workspace_export_resources(
&db, &db,
@@ -21,9 +23,5 @@ pub fn export_data(params: ExportDataParams<'_>) -> Result<()> {
params.include_private_environments, params.include_private_environments,
)?; )?;
let file = File::options().create(true).truncate(true).write(true).open(params.export_path)?; Ok(serde_json::to_string_pretty(&export_data)?)
serde_json::to_writer_pretty(&file, &export_data)?;
file.sync_all()?;
Ok(())
} }
+7 -1
View File
@@ -224,6 +224,13 @@ const HANDLERS: Partial<Record<AppCmd, Handler>> = {
// The rows the sender wrote for that response, same table as the desktop. // The rows the sender wrote for that response, same table as the desktop.
cmd_get_http_response_events: (payload, db) => db.rpc("cmd_get_http_response_events", payload), cmd_get_http_response_events: (payload, db) => db.rpc("cmd_get_http_response_events", payload),
/* ------------------------------- export -------------------------------- */
// Built by the model layer, exactly as the desktop and the CLI build it. Where
// the document goes is not decided here: the caller hands it to `files.save`,
// which is the one thing each host answers differently.
cmd_export_data: (payload, db) => db.rpc("cmd_export_data", payload),
async cmd_get_sse_events() { async cmd_get_sse_events() {
return []; return [];
}, },
@@ -282,7 +289,6 @@ const DECLINED: Partial<Record<AppCmd, [reason: string, capability: CapabilityNa
cmd_commit_import: ["Importing needs a plugin, which this host doesn't run", null], cmd_commit_import: ["Importing needs a plugin, which this host doesn't run", null],
cmd_list_import_sources: ["Importing isn't available in the browser yet", null], cmd_list_import_sources: ["Importing isn't available in the browser yet", null],
cmd_import_sources_for_origin: ["Importing isn't available in the browser yet", null], cmd_import_sources_for_origin: ["Importing isn't available in the browser yet", null],
cmd_export_data: ["Exporting to a file isn't available in the browser yet", "localFiles"],
cmd_create_example_workspace: ["The example workspace isn't available in the browser yet", null], cmd_create_example_workspace: ["The example workspace isn't available in the browser yet", null],
cmd_save_response: ["Saving a response to disk isn't available in the browser", "localFiles"], cmd_save_response: ["Saving a response to disk isn't available in the browser", "localFiles"],
cmd_save_base64_to_binary: ["Saving to disk isn't available in the browser", "localFiles"], cmd_save_base64_to_binary: ["Saving to disk isn't available in the browser", "localFiles"],