//! Uniform result shape for every command verb, shared by the CLI binary and
//! the desktop console panel. Both entry points run a verb, get back one of
//! these, and then just print it (human line, or `--json`).

/// Exit codes are the stable contract documented in the plan's Global
/// Constraints:
/// 0 success · 1 operation refused (`-f` class) · 2 usage/parse error ·
/// 3 auth failure · 4 connection/TLS/pin failure · 5 context error
/// (no project/workspace).
#[derive(Debug, Clone)]
pub struct CommandOutput {
    /// Whether the command succeeded.
    pub ok: bool,
    /// Process exit code (see the table above).
    pub code: i32,
    /// Exactly what the CLI prints on stdout in human mode.
    pub human: String,
    /// Machine-readable payload for `--json`; `serde_json::Value::Null` when
    /// a verb has nothing structured to report.
    pub data: serde_json::Value,
    /// Refresh hints for the desktop console: which panels should re-fetch
    /// after this command ran (e.g. `"files"`, `"locks"`, `"users"`,
    /// `"projects"`). Empty for read-only / failed commands.
    pub changed: Vec<&'static str>,
}

impl CommandOutput {
    /// Renders the `--json` line: `{"ok":bool,"code":int,"data":...,"error":str|null}`.
    ///
    /// Field mapping:
    /// - `ok`, `code`, `data` are passed through verbatim.
    /// - `error` is `null` when `ok` is `true`; otherwise it is the `human`
    ///   string (the same actionable message the non-JSON caller would have
    ///   printed) — there is no separate machine error message, `human` is
    ///   already required to name the fix.
    pub fn to_json_line(&self) -> String {
        let error = if self.ok {
            serde_json::Value::Null
        } else {
            serde_json::Value::String(self.human.clone())
        };
        let payload = serde_json::json!({
            "ok": self.ok,
            "code": self.code,
            "data": self.data,
            "error": error,
        });
        payload.to_string()
    }
}
