use std::time::Duration;
use anyhow::Context;
use serde_json::{json, Value};
use sylphx_sdk_core::normalize_management_base_url;
use sylphx_sdk_management::ManagementClient;
use crate::channel;
use crate::context;
use crate::credentials;
use crate::ux;
pub async fn doctor(
json: bool,
client: &ManagementClient,
token: &str,
base: &str,
) -> anyhow::Result<()> {
#[derive(serde::Serialize)]
struct Row {
status: &'static str,
title: String,
detail: String,
#[serde(skip_serializing_if = "Option::is_none")]
fix: Option<String>,
}
let mut rows: Vec<Row> = Vec::new();
let mut worst = "pass";
let channel = channel::detect_install_channel();
let current = env!("CARGO_PKG_VERSION");
rows.push(Row {
status: "pass",
title: "CLI version".into(),
detail: format!("{current} (Rust)"),
fix: None,
});
rows.push(Row {
status: "pass",
title: "Install channel".into(),
detail: format!(
"{} · upgrade: {}",
channel.as_str(),
channel.upgrade_command()
),
fix: None,
});
match channel::fetch_latest_release_version().await {
Ok(latest) => {
if channel::is_outdated(current, &latest) {
if worst == "pass" {
worst = "warn";
}
rows.push(Row {
status: "warn",
title: "Update available".into(),
detail: format!("{current} → {latest}"),
fix: Some(channel.upgrade_command().to_string()),
});
} else if channel::is_outdated(&latest, current) {
rows.push(Row {
status: "pass",
title: "Update available".into(),
detail: format!("local {current} is ahead of published {latest}"),
fix: None,
});
} else {
rows.push(Row {
status: "pass",
title: "Update available".into(),
detail: format!("up to date with {latest}"),
fix: None,
});
}
}
Err(e) => {
rows.push(Row {
status: "pass",
title: "Update available".into(),
detail: format!("skipped ({e})"),
fix: None,
});
}
}
let kind = credentials::token_kind(token);
rows.push(Row {
status: "pass",
title: "Credential".into(),
detail: format!("kind={kind}"),
fix: None,
});
rows.push(Row {
status: "pass",
title: "API base URL".into(),
detail: normalize_management_base_url(base),
fix: None,
});
match client.health().await {
Ok(h) => rows.push(Row {
status: "pass",
title: "API health".into(),
detail: format!("status={}", h.status),
fix: None,
}),
Err(e) => {
worst = "fail";
rows.push(Row {
status: "fail",
title: "API health".into(),
detail: e.to_string(),
fix: Some("check network / SYLPHX_API_URL".into()),
});
}
}
match client.whoami().await {
Ok(w) => {
let email = w.user.as_ref().map(|u| u.email.as_str()).unwrap_or("-");
rows.push(Row {
status: "pass",
title: "Whoami".into(),
detail: format!("{email} · {} orgs", w.orgs.len()),
fix: None,
});
}
Err(e) => {
let msg = e.to_string();
if msg.contains("user_context_required") && kind == "service_token" {
rows.push(Row {
status: "pass",
title: "Whoami".into(),
detail: "service token (no user profile — expected)".into(),
fix: None,
});
} else {
worst = "fail";
rows.push(Row {
status: "fail",
title: "Whoami".into(),
detail: msg,
fix: Some("sylphx login".into()),
});
}
}
}
match context::resolve_org_id(None)? {
Some(o) => rows.push(Row {
status: "pass",
title: "Preferred org".into(),
detail: o,
fix: None,
}),
None => {
if worst == "pass" {
worst = "warn";
}
rows.push(Row {
status: "warn",
title: "Preferred org".into(),
detail: "not set — projects list may be empty".into(),
fix: Some("sylphx context use --org-id <org_…>".into()),
});
}
}
match context::load_linked_project()? {
Some(l) => rows.push(Row {
status: "pass",
title: "Linked project".into(),
detail: format!(
"{} ({})",
l.project_id,
l.project_slug.as_deref().unwrap_or("-")
),
fix: None,
}),
None => rows.push(Row {
status: "pass",
title: "Linked project".into(),
detail: "none (optional)".into(),
fix: None,
}),
}
let body = json!({ "worst": worst, "checks": rows, "installChannel": channel.as_str() });
ux::print_out(json, &body, || {
println!("sylphx doctor");
for r in &rows {
let mark = match r.status {
"pass" => "✓",
"warn" => "!",
_ => "✗",
};
println!(" {mark} {} — {}", r.title, r.detail);
if let Some(fix) = &r.fix {
println!(" fix: {fix}");
}
}
println!();
println!("summary: {worst}");
});
if worst == "fail" {
anyhow::bail!("doctor found failures — see above");
}
Ok(())
}
pub async fn update(
json: bool,
check_only: bool,
version: Option<String>,
force_release_binary: bool,
) -> anyhow::Result<()> {
let channel = channel::detect_install_channel();
let current = env!("CARGO_PKG_VERSION");
let latest = channel::fetch_latest_release_version().await.ok();
let ver = version
.clone()
.unwrap_or_else(|| latest.clone().unwrap_or_else(|| "latest".into()));
let os = std::env::consts::OS;
let arch = match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
other => other,
};
let asset = match os {
"linux" => format!("sylphx-linux-{arch}"),
"macos" => format!("sylphx-darwin-{arch}"),
other => {
anyhow::bail!(
"unsupported os={other}; use cargo install sylphx-cli or npm i -g @sylphx/cli"
)
}
};
let tag = if ver == "latest" {
latest
.as_deref()
.map(|v| format!("cli-v{v}"))
.unwrap_or_else(|| "latest".into())
} else if ver.starts_with("cli-v") || ver.starts_with('v') {
ver.clone()
} else {
format!("cli-v{ver}")
};
let url = if tag == "latest" {
format!("https://github.com/SylphxAI/platform/releases/latest/download/{asset}")
} else {
format!("https://github.com/SylphxAI/platform/releases/download/{tag}/{asset}")
};
let target_ver = latest.clone().unwrap_or_else(|| ver.clone());
let outdated = latest
.as_deref()
.map(|l| channel::is_outdated(current, l))
.unwrap_or(false);
if check_only {
let body = json!({
"current": current,
"latest": latest,
"target": target_ver,
"outdated": outdated,
"installChannel": channel.as_str(),
"upgradeCommand": channel.upgrade_command(),
"releaseSelfUpdateAllowed": channel.allows_release_self_update() || force_release_binary,
"asset": asset,
"url": url,
"checkOnly": true,
});
ux::print_out(json, &body, || {
println!("current {current}");
if let Some(l) = &latest {
println!("latest {l}");
println!("outdated {outdated}");
}
println!("channel {}", channel.as_str());
println!("upgrade {}", channel.upgrade_command());
println!("download {url}");
if !channel.allows_release_self_update() {
println!();
println!(
"Note: this install looks like '{}' — prefer package-manager upgrade.",
channel.as_str()
);
println!("In-place release replace requires: sylphx update --force-release-binary");
}
});
return Ok(());
}
if !channel.allows_release_self_update() && !force_release_binary {
let body = json!({
"ok": false,
"error": "channel_refuses_release_self_update",
"installChannel": channel.as_str(),
"upgradeCommand": channel.upgrade_command(),
"hint": "re-run with --force-release-binary to replace this executable from GitHub Releases anyway",
});
ux::print_out(json, &body, || {
println!(
"refusing in-place release self-update for channel '{}'",
channel.as_str()
);
println!(" upgrade with: {}", channel.upgrade_command());
println!(" or force: sylphx update --force-release-binary");
});
anyhow::bail!(
"install channel '{}' must upgrade via: {}",
channel.as_str(),
channel.upgrade_command()
);
}
let dest = std::env::current_exe().context("resolve current executable")?;
let tmp = dest.with_extension("download");
let client = reqwest::Client::builder()
.user_agent(format!(
"sylphx-cli/{current} (+https://sylphx.com)"
))
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.context("http client")?;
let resp = client.get(&url).send().await.context("download release")?;
if !resp.status().is_success() {
anyhow::bail!(
"download failed HTTP {} for {url}\n\
Fallback: {}\n\
or: curl -fsSL https://raw.githubusercontent.com/SylphxAI/platform/main/scripts/install-sylphx.sh | bash",
resp.status(),
channel.upgrade_command()
);
}
let bytes = resp.bytes().await.context("read body")?;
std::fs::write(&tmp, &bytes).with_context(|| format!("write {}", tmp.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))?;
}
let probe = std::process::Command::new(&tmp)
.arg("--version")
.output()
.context("probe downloaded binary")?;
if !probe.status.success() {
let _ = std::fs::remove_file(&tmp);
anyhow::bail!("downloaded asset is not a runnable sylphx binary");
}
let new_ver = String::from_utf8_lossy(&probe.stdout).trim().to_string();
std::fs::rename(&tmp, &dest).with_context(|| {
format!(
"replace {} — permission denied? try: {}",
dest.display(),
channel.upgrade_command()
)
})?;
let body = json!({
"ok": true,
"previous": current,
"installed": new_ver,
"path": dest.display().to_string(),
"url": url,
"installChannel": channel.as_str(),
"forcedReleaseBinary": force_release_binary,
});
ux::print_out(json, &body, || {
ux::ok_line(&format!("updated → {new_ver}"));
println!(" path {}", dest.display());
println!(" channel {}", channel.as_str());
});
Ok(())
}
pub async fn wait_status(
json: bool,
client: &ManagementClient,
project_id: &str,
timeout_secs: u64,
interval_secs: u64,
org_id: Option<&str>,
) -> anyhow::Result<()> {
let start = std::time::Instant::now();
let timeout = Duration::from_secs(timeout_secs);
let interval = Duration::from_secs(interval_secs.max(1));
loop {
let status = client
.get_project_status(project_id, org_id)
.await
.map_err(ux::map_sdk_err)?;
let val = serde_json::to_value(&status)?;
let state = val
.pointer("/status")
.or_else(|| val.pointer("/deployment/status"))
.or_else(|| val.pointer("/current/status"))
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
if !json {
println!(
"wait project={project_id} state={state} elapsed={}s",
start.elapsed().as_secs()
);
}
let terminal = matches!(
state.to_ascii_lowercase().as_str(),
"ready" | "live" | "healthy" | "succeeded" | "success" | "failed" | "error" | "cancelled"
);
if terminal {
ux::print_out(json, &val, || ux::ok_line(&format!("terminal state={state}")));
if matches!(
state.to_ascii_lowercase().as_str(),
"failed" | "error" | "cancelled"
) {
anyhow::bail!("deployment reached failure state: {state}");
}
return Ok(());
}
if start.elapsed() > timeout {
ux::print_out(json, &val, || {
println!("timeout waiting for project {project_id} (last state={state})");
});
anyhow::bail!("wait timed out after {timeout_secs}s");
}
tokio::time::sleep(interval).await;
}
}
pub async fn api_json(
client: &ManagementClient,
method: reqwest::Method,
path: &str,
body: Option<&Value>,
org_id: Option<&str>,
) -> anyhow::Result<(u16, Value)> {
client
.api(method, path, body, org_id)
.await
.map_err(ux::map_sdk_err)
}
pub fn enc(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
async fn emit_api(
json: bool,
client: &ManagementClient,
method: reqwest::Method,
path: &str,
body: Option<&Value>,
org: Option<&str>,
) -> anyhow::Result<()> {
let (status, body) = api_json(client, method, path, body, org).await?;
ux::print_out(json, &json!({"status": status, "body": body}), || {
if !json {
println!("HTTP {status}");
}
println!("{}", serde_json::to_string_pretty(&body).unwrap_or_default());
});
Ok(())
}
pub async fn rest_get(
json: bool,
client: &ManagementClient,
path: &str,
org: Option<&str>,
) -> anyhow::Result<()> {
emit_api(json, client, reqwest::Method::GET, path, None, org).await
}
pub async fn rest_mut(
json: bool,
client: &ManagementClient,
method: reqwest::Method,
path: &str,
body: Option<&Value>,
org: Option<&str>,
) -> anyhow::Result<()> {
emit_api(json, client, method, path, body, org).await
}
pub async fn inspect_project(
json: bool,
client: &ManagementClient,
project: &str,
org: Option<&str>,
) -> anyhow::Result<()> {
let status = client
.get_project_status(project, org)
.await
.map_err(ux::map_sdk_err)?;
let project_obj = client
.get_project(project, org)
.await
.map_err(ux::map_sdk_err)?;
let envs = client
.list_environments(project, None, org)
.await
.map_err(ux::map_sdk_err)?;
let deploys = client
.list_deployments(project, None, org)
.await
.map_err(ux::map_sdk_err)?;
let (svc_status, services) = api_json(
client,
reqwest::Method::GET,
&format!("/projects/{}/services", enc(project)),
None,
org,
)
.await
.unwrap_or((0, Value::Null));
let body = json!({
"project": project_obj,
"status": status,
"environments": envs,
"deployments": deploys,
"services": { "status": svc_status, "body": services },
});
ux::print_out(json, &body, || {
println!("inspect {project}");
println!("{}", serde_json::to_string_pretty(&body).unwrap_or_default());
});
Ok(())
}
pub fn catalog_entries() -> Vec<Value> {
let rows = [
("health", "first-class", "GET /health"),
("login/logout/whoami", "first-class", "auth bootstrap"),
("doctor/config/context/link/open/update", "first-class", "operator UX"),
("orgs list", "first-class", "from whoami"),
("projects list|get|create", "first-class", "/projects"),
("environments list|get", "first-class", "/projects/:id/environments"),
("deployments list", "first-class", "/projects/:id/deployments"),
("deploy|rollback|status|wait", "first-class", "deploy loop"),
("env list|set|rm", "first-class", "/projects/:id/env-vars"),
("logs|tail", "first-class", "/projects/:id/logs"),
("resources *", "first-class", "/resources"),
("db *", "first-class", "resources kind=database"),
("storage *", "first-class", "/projects/:id/storage"),
("tasks *", "first-class", "/tasks"),
("backup *", "first-class", "/backups"),
("flags *", "first-class", "/flags"),
("previews cost|list", "first-class", "/projects/:id/previews"),
("domains *", "first-class", "/projects/:id/domains"),
("secrets *", "first-class", "/secrets|/projects/:id/secrets"),
("tokens *", "first-class", "/orgs/:id/service-tokens"),
("billing usage|plan", "first-class", "/billing/*"),
("services *", "first-class", "/projects/:id/services"),
("webhooks *", "first-class", "/webhooks|/projects/:id/webhooks"),
("certs list", "first-class", "/projects/:id/certs"),
("releases list", "first-class", "/projects/:id/releases"),
("runners list", "first-class", "/runners"),
("sandbox list|get|create", "first-class", "/sandboxes"),
("volumes list", "first-class", "/volumes"),
("users me", "first-class", "/users/me|/whoami"),
("inspect", "first-class", "project summary bundle"),
("init", "first-class", "scaffold + optional link"),
("catalog", "first-class", "this surface map"),
("admin *", "first-class", "/admin/* via passthrough"),
("delivery *", "first-class", "delivery control plane via /delivery/*"),
("api METHOD PATH", "escape-hatch", "any Management REST route"),
("account/ai/analytics/consent/email/engagement", "api-escape", "use: sylphx api get /…"),
("experiments/insights/monitoring/newsletter", "api-escape", "use: sylphx api get /…"),
("notifications/oidc/plan/privacy/promote", "api-escape", "use: sylphx api …"),
("realtime/referrals/saml/search/session-replay", "api-escape", "use: sylphx api …"),
("workspaces/device-sessions/mobile-device-tests", "api-escape", "use: sylphx api …"),
("migrate/bisect/dev/run", "out-of-band", "local/dev tooling — not Management SSOT"),
];
rows
.into_iter()
.map(|(command, kind, notes)| {
json!({
"command": command,
"kind": kind,
"notes": notes,
})
})
.collect()
}
pub fn print_catalog(json_mode: bool) {
let entries = catalog_entries();
ux::print_out(json_mode, &json!({ "surface": entries, "plane": "management" }), || {
println!("Sylphx Management operator surface catalog");
println!("plane: management (api.sylphx.com/v1) — not BaaS app runtime");
println!();
for e in &entries {
println!(
" {:40} {:14} {}",
e["command"].as_str().unwrap_or("-"),
e["kind"].as_str().unwrap_or("-"),
e["notes"].as_str().unwrap_or("-"),
);
}
println!();
println!("Any residual Management route: sylphx api get|post|… /path --org-id …");
});
}
pub async fn init_project(
json: bool,
name: &str,
org_id: Option<&str>,
link: bool,
) -> anyhow::Result<()> {
let dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let sylphx_dir = dir.join(".sylphx");
std::fs::create_dir_all(&sylphx_dir)?;
let marker = sylphx_dir.join("init.json");
let body = json!({
"name": name,
"orgId": org_id,
"createdAt": chrono_like_now(),
"cli": env!("CARGO_PKG_VERSION"),
});
std::fs::write(&marker, serde_json::to_string_pretty(&body)?)?;
let gi = dir.join(".gitignore");
if gi.exists() {
let raw = std::fs::read_to_string(&gi).unwrap_or_default();
if !raw.lines().any(|l| l.trim() == ".sylphx/") {
let mut n = raw;
if !n.ends_with('\n') && !n.is_empty() {
n.push('\n');
}
n.push_str("# Sylphx local link/context\n.sylphx/\n");
let _ = std::fs::write(&gi, n);
}
}
let out = json!({
"ok": true,
"path": marker.display().to_string(),
"name": name,
"orgId": org_id,
"linkHint": if link { "pass --project after create to link" } else { "sylphx link --project <id>" },
});
ux::print_out(json, &out, || {
ux::ok_line(&format!("initialized .sylphx for {name}"));
println!(" {}", marker.display());
println!("Next: sylphx projects create {name} --org-id <org> && sylphx link --project <id>");
});
Ok(())
}
fn chrono_like_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("{secs}")
}
pub async fn domains_list(json: bool, client: &ManagementClient, project: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/projects/{}/domains", enc(project)), org).await
}
pub async fn domains_get(json: bool, client: &ManagementClient, project: &str, domain_id: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/projects/{}/domains/{}", enc(project), enc(domain_id)), org).await
}
pub async fn domains_add(json: bool, client: &ManagementClient, project: &str, domain: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_mut(json, client, reqwest::Method::POST, &format!("/projects/{}/domains", enc(project)), Some(&json!({"domain": domain})), org).await
}
pub async fn domains_rm(json: bool, client: &ManagementClient, project: &str, domain_id: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_mut(json, client, reqwest::Method::DELETE, &format!("/projects/{}/domains/{}", enc(project), enc(domain_id)), None, org).await
}
pub async fn secrets_list(json: bool, client: &ManagementClient, project: Option<&str>, org: Option<&str>) -> anyhow::Result<()> {
let path = if let Some(p) = project.filter(|s| !s.is_empty()) {
format!("/projects/{}/secrets", enc(p))
} else if let Some(o) = org {
format!("/orgs/{}/secrets", enc(o))
} else {
"/secrets".into()
};
rest_get(json, client, &path, org).await
}
pub async fn secrets_set(json: bool, client: &ManagementClient, name: &str, value: &str, project: Option<&str>, org: Option<&str>) -> anyhow::Result<()> {
let path = if let Some(p) = project.filter(|s| !s.is_empty()) {
format!("/projects/{}/secrets", enc(p))
} else {
"/secrets".into()
};
rest_mut(json, client, reqwest::Method::POST, &path, Some(&json!({"name": name, "value": value})), org).await
}
pub async fn secrets_rm(json: bool, client: &ManagementClient, name: &str, project: Option<&str>, org: Option<&str>) -> anyhow::Result<()> {
let path = if let Some(p) = project.filter(|s| !s.is_empty()) {
format!("/projects/{}/secrets/{}", enc(p), enc(name))
} else {
format!("/secrets/{}", enc(name))
};
rest_mut(json, client, reqwest::Method::DELETE, &path, None, org).await
}
pub async fn tokens_list(json: bool, client: &ManagementClient, org: &str) -> anyhow::Result<()> {
rest_get(json, client, &format!("/orgs/{}/service-tokens", enc(org)), Some(org)).await
}
pub async fn tokens_create(json: bool, client: &ManagementClient, org: &str, name: &str, scopes: &str, project: Option<&str>) -> anyhow::Result<()> {
let scope_list: Vec<&str> = scopes.split(',').map(str::trim).filter(|s| !s.is_empty()).collect();
let mut body = json!({"name": name, "scopes": scope_list});
if let Some(p) = project { body["projectId"] = json!(p); }
rest_mut(json, client, reqwest::Method::POST, &format!("/orgs/{}/service-tokens", enc(org)), Some(&body), Some(org)).await
}
pub async fn tokens_rm(json: bool, client: &ManagementClient, org: &str, token_id: &str) -> anyhow::Result<()> {
rest_mut(json, client, reqwest::Method::DELETE, &format!("/orgs/{}/service-tokens/{}", enc(org), enc(token_id)), None, Some(org)).await
}
pub async fn tokens_rotate(json: bool, client: &ManagementClient, org: &str, token_id: &str) -> anyhow::Result<()> {
rest_mut(json, client, reqwest::Method::POST, &format!("/orgs/{}/service-tokens/{}/rotate", enc(org), enc(token_id)), Some(&json!({})), Some(org)).await
}
pub async fn billing_usage(json: bool, client: &ManagementClient, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, "/billing/usage", org).await
}
pub async fn billing_plan(json: bool, client: &ManagementClient, org: Option<&str>) -> anyhow::Result<()> {
let path = if let Some(o) = org {
format!("/orgs/{}/billing/plan", enc(o))
} else {
"/billing/plan".into()
};
rest_get(json, client, &path, org).await
}
pub async fn services_list(json: bool, client: &ManagementClient, project: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/projects/{}/services", enc(project)), org).await
}
pub async fn webhooks_list(json: bool, client: &ManagementClient, project: Option<&str>, org: Option<&str>) -> anyhow::Result<()> {
let path = if let Some(p) = project.filter(|s| !s.is_empty()) {
format!("/projects/{}/webhooks", enc(p))
} else {
"/webhooks".into()
};
rest_get(json, client, &path, org).await
}
pub async fn certs_list(json: bool, client: &ManagementClient, project: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/projects/{}/certs", enc(project)), org).await
}
pub async fn releases_list(json: bool, client: &ManagementClient, project: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/projects/{}/releases", enc(project)), org).await
}
pub async fn runners_list(json: bool, client: &ManagementClient, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, "/runners", org).await
}
pub async fn sandbox_list(json: bool, client: &ManagementClient, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, "/sandboxes", org).await
}
pub async fn sandbox_get(json: bool, client: &ManagementClient, id: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/sandboxes/{}", enc(id)), org).await
}
pub async fn sandbox_create(json: bool, client: &ManagementClient, name: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_mut(json, client, reqwest::Method::POST, "/sandboxes", Some(&json!({"name": name})), org).await
}
pub async fn volumes_list(json: bool, client: &ManagementClient, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, "/volumes", org).await
}
pub async fn users_me(json: bool, client: &ManagementClient, token: &str) -> anyhow::Result<()> {
match client.whoami().await {
Ok(w) => {
ux::print_out(json, &w, || {
println!("{}", serde_json::to_string_pretty(&w).unwrap_or_default());
});
Ok(())
}
Err(e) => {
let msg = e.to_string();
if msg.contains("user_context_required")
|| credentials::token_kind(token) == "service_token"
{
let body = serde_json::json!({
"principal": "service_token",
"tokenKind": credentials::token_kind(token),
"note": "Service tokens have no user profile. Use `sylphx login` for a user JWT."
});
ux::print_out(json, &body, || {
println!("Authenticated with service token (no user profile)");
println!(" token {}", credentials::token_kind(token));
println!("Tip: sylphx login");
});
return Ok(());
}
match api_json(client, reqwest::Method::GET, "/users/me", None, None).await {
Ok((status, body)) => {
ux::print_out(json, &serde_json::json!({"status": status, "body": body}), || {
println!("{}", serde_json::to_string_pretty(&body).unwrap_or_default());
});
Ok(())
}
Err(_) => Err(ux::map_sdk_err(e)),
}
}
}
}
pub async fn previews_list(json: bool, client: &ManagementClient, project: &str, org: Option<&str>) -> anyhow::Result<()> {
rest_get(json, client, &format!("/projects/{}/previews", enc(project)), org).await
}
pub async fn admin_get(json: bool, client: &ManagementClient, path: &str, org: Option<&str>) -> anyhow::Result<()> {
let p = if path.starts_with('/') { path.to_string() } else { format!("/{path}") };
let full = if p.starts_with("/admin") { p } else { format!("/admin{p}") };
rest_get(json, client, &full, org).await
}
pub async fn delivery_get(json: bool, client: &ManagementClient, path: &str, org: Option<&str>) -> anyhow::Result<()> {
let p = if path.starts_with('/') { path.to_string() } else { format!("/{path}") };
let full = if p.starts_with("/delivery") { p } else { format!("/delivery{p}") };
rest_get(json, client, &full, org).await
}