Skip to main content

objectiveai_cli/command/viewer/
spawn.rs

1//! `viewer spawn` — start the `objectiveai-viewer` Tauri shell in the
2//! background.
3//!
4//! The viewer is per-state: its lock lives at
5//! `<dir>/state/<state>/locks` key `viewer`, and the lock contents are
6//! the server's client-connect URL. If the lock is already held the
7//! viewer is already up and its published URL is returned as-is.
8
9use objectiveai_sdk::cli::command::viewer::spawn::{Request, Response};
10
11use crate::context::Context;
12use crate::error::Error;
13
14/// The spawn flow itself, callable in-process (used by
15/// `Context::viewer_client()` as well as the `viewer spawn` command).
16/// Idempotent and cheap when the viewer is already up: a try_read of
17/// the lock returns the published URL without spawning.
18pub async fn spawn(ctx: &Context) -> Result<String, Error> {
19    let mut config = ctx
20        .filesystem
21        .read_config_view(objectiveai_sdk::cli::command::GetScope::Final)
22        .await?;
23    let secret = config.viewer().get_secret().map(String::from);
24
25    let bin = if cfg!(windows) {
26        "objectiveai-viewer.exe"
27    } else {
28        "objectiveai-viewer"
29    };
30    let exe = ctx.filesystem.bin_dir().join(bin);
31    let lock_dir = ctx.filesystem.state_dir().join("locks");
32
33    // The child inherits the cli's environment; every env key the
34    // viewer's config reads (`EnvConfigBuilder` in
35    // `objectiveai-viewer/src-tauri/src/run.rs`: ADDRESS, PORT,
36    // VIEWER_SECRET, SUPPRESS_OUTPUT, OBJECTIVEAI_DIR,
37    // OBJECTIVEAI_STATE) is either set explicitly here or scrubbed,
38    // so the spawning shell's settings can't leak in. ADDRESS/PORT
39    // are always scrubbed — the viewer defaults to 127.0.0.1 on an
40    // ephemeral port and publishes the bound URL in its lock. The
41    // secret comes from on-disk config, scrubbed when unset so the
42    // viewer falls back to its own default.
43    crate::spawn::spawn_until_lock_published(&exe, &lock_dir, "viewer", |cmd| {
44        cmd.env_remove("ADDRESS");
45        cmd.env_remove("PORT");
46        cmd.env_remove("VIEWER_SECRET");
47        cmd.env("OBJECTIVEAI_DIR", ctx.filesystem.dir())
48            .env("OBJECTIVEAI_STATE", ctx.filesystem.state())
49            .env("SUPPRESS_OUTPUT", "true");
50        if let Some(secret) = secret {
51            cmd.env("VIEWER_SECRET", secret);
52        }
53    })
54    .await
55}
56
57pub async fn execute(ctx: &Context, _request: Request) -> Result<Response, Error> {
58    Ok(Response {
59        listening: spawn(ctx).await?,
60    })
61}
62
63pub mod request_schema {
64    use objectiveai_sdk::cli::command::viewer::spawn as sdk;
65    use objectiveai_sdk::cli::command::viewer::spawn::request_schema::{Request, Response};
66
67    use crate::context::Context;
68    use crate::error::Error;
69
70    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
71        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
72    }
73}
74
75pub mod response_schema {
76    use objectiveai_sdk::cli::command::viewer::spawn as sdk;
77    use objectiveai_sdk::cli::command::viewer::spawn::response_schema::{Request, Response};
78
79    use crate::context::Context;
80    use crate::error::Error;
81
82    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
83        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
84    }
85}