use std::path::Path;
use clap::CommandFactory;
use serde_json::{json, Value};
use crate::client::ApiClient;
use crate::config::{PartiriConfig, CONFIG_FILE};
use crate::error::{CliError, Result};
use crate::output::{ctx, print_result};
pub const LLM_GUIDE: &str = include_str!("../../LLM.md");
pub fn run_guide() -> Result<()> {
if ctx().json {
print_result(&json!({ "markdown": LLM_GUIDE }));
} else {
print!("{}", LLM_GUIDE);
}
Ok(())
}
pub fn run_schema() -> Result<()> {
let schema = config_schema();
if ctx().json {
print_result(&schema);
} else {
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
Ok(())
}
fn config_schema() -> Value {
json!({
"title": ".partiri.jsonc",
"description": "Per-service config file consumed by the partiri CLI.",
"type": "object",
"required": ["fk_workspace", "fk_project", "service"],
"properties": {
"id": { "type": ["string", "null"], "description": "Service UUID. Set by 'partiri service create'; do not edit by hand." },
"deploy_tag": { "type": ["string", "null"], "description": "Most recent deploy tag. Set by 'partiri service deploy'." },
"fk_workspace": { "type": "string", "description": "Workspace UUID." },
"fk_project": { "type": "string", "description": "Project UUID. Must belong to fk_workspace." },
"service": {
"type": "object",
"required": ["name", "deploy_type", "runtime", "root_path", "fk_region", "fk_pod"],
"properties": {
"name": { "type": "string", "maxLength": 16, "description": "Service name. Unique within a project." },
"deploy_type": { "type": "string", "enum": ["webservice", "static", "private-service"] },
"runtime": { "type": "string", "enum": ["node", "rust", "python", "go", "ruby", "elixir", "php", "jvm", "dotnet", "cpp", "static", "registry"] },
"root_path": { "type": "string", "default": "." },
"repository_url": { "type": ["string", "null"], "description": "Mutually exclusive with registry_url." },
"repository_branch": { "type": ["string", "null"], "description": "Required when repository_url is set." },
"registry_url": { "type": ["string", "null"], "description": "Mutually exclusive with repository_url." },
"registry_repository_url": { "type": ["string", "null"], "description": "Required when registry_url is set." },
"fk_service_secret": { "type": ["string", "null"], "description": "Required for private repository or registry sources." },
"build_path": { "type": ["string", "null"] },
"build_command": { "type": ["string", "null"], "description": "Required for repo source on non-static runtimes." },
"pre_deploy_command": { "type": ["string", "null"] },
"run_command": { "type": ["string", "null"], "description": "Required for deploy_type=webservice or private-service." },
"fk_region": { "type": "string" },
"fk_pod": { "type": "string", "description": "Must come from the same workspace as fk_region." },
"health_check_path": { "type": ["string", "null"], "description": "Path or absolute URL. Only absolute URLs are probed by 'validate --remote'." },
"maintenance_mode": { "type": "boolean", "default": false },
"active": { "type": "boolean", "default": true },
"env": {
"type": "array",
"items": {
"type": "object",
"required": ["key", "value"],
"properties": {
"key": { "type": "string" },
"value": { "type": "string" }
}
}
}
}
}
},
"rules": [
"service.repository_url XOR service.registry_url (one must be set, not both).",
"When deploy_type ∈ {webservice, private-service}, service.run_command is required.",
"When repository_url is set on a non-static runtime, service.build_command is required.",
"service.name must be ≤16 characters.",
"fk_region and fk_pod must belong to the same workspace.",
]
})
}
pub struct TemplateArgs {
pub deploy_type: Option<String>,
pub runtime: Option<String>,
pub source: Option<String>,
}
pub fn run_template(args: TemplateArgs) -> Result<()> {
let dt = args.deploy_type.as_deref().unwrap_or("webservice");
let runtime = args.runtime.as_deref().unwrap_or("node");
let source = args.source.as_deref().unwrap_or("repo");
let template = build_template(dt, runtime, source);
if ctx().json {
print_result(&json!({
"deploy_type": dt,
"runtime": runtime,
"source": source,
"template": template,
}));
} else {
println!("{}", template);
}
Ok(())
}
fn build_template(deploy_type: &str, runtime: &str, source: &str) -> String {
let (build_command, run_command) = default_commands(runtime);
let is_registry = source == "registry";
let build_line = if is_registry {
" // \"build_command\": null, // not used for registry-sourced deploys (image is pre-built)".to_string()
} else {
format!(" \"build_command\": \"{}\",", build_command)
};
let run_line = if deploy_type == "static" {
" // \"run_command\": null, // not used for static deploys".to_string()
} else if is_registry {
" // \"run_command\": null, // not used for registry-sourced deploys (image entrypoint runs)".to_string()
} else {
format!(" \"run_command\": \"{}\",", run_command)
};
let source_block = if is_registry {
r#" "registry_url": "registry.example.com",
"registry_repository_url": "your-org/your-image:tag",
// "repository_url": null,
// "repository_branch": null,
// For private images, set fk_service_secret to a registry-secret UUID:
// "fk_service_secret": "uuid", // run 'partiri service token --secret <UUID>'"#
} else {
r#" "repository_url": "https://github.com/your-org/your-repo.git",
"repository_branch": "main",
// "registry_url": null,
// "registry_repository_url": null,
// For private repos, set fk_service_secret to a repository-secret UUID:
// "fk_service_secret": "uuid", // run 'partiri service token --secret <UUID>'"#
};
format!(
r#"{{
// The service ID is assigned by 'partiri service create'; leave null until then.
"id": null,
"deploy_tag": null,
"fk_workspace": "<workspace UUID — run 'partiri -j llm context' to discover>",
"fk_project": "<project UUID — same source>",
"service": {{
"name": "my-service", // ≤16 chars
"deploy_type": "{deploy_type}", // webservice | static | private-service
"runtime": "{runtime}",
"root_path": ".",
{source_block}
{build_line}
// "build_path": "dist",
// "pre_deploy_command": "",
{run_line}
"fk_region": "<region UUID>",
"fk_pod": "<pod UUID>",
// "health_check_path": "/health",
"maintenance_mode": false,
"active": true,
"env": []
}}
}}
"#
)
}
fn default_commands(runtime: &str) -> (&'static str, &'static str) {
match runtime {
"node" => ("npm run build", "node ./dist/server/entry.mjs"),
"rust" => ("cargo build --release", "./target/release/app"),
"python" => ("pip install -r requirements.txt", "python main.py"),
"go" => ("go build -o app .", "./app"),
"ruby" => ("bundle install", "ruby app.rb"),
"elixir" => ("mix deps.get && mix compile", "mix phx.server"),
"php" => ("composer install", "php -S 0.0.0.0:$PORT"),
"jvm" => ("./gradlew build", "java -jar build/libs/app.jar"),
"dotnet" => (
"dotnet publish -c Release",
"dotnet bin/Release/net8.0/app.dll",
),
"cpp" => ("make", "./app"),
"static" => ("npm run build", ""),
"registry" => ("", ""),
_ => ("", ""),
}
}
pub fn run_examples() -> Result<()> {
let examples = json!([
{
"name": "node-webservice-public-repo",
"description": "Node.js webservice from a public GitHub repo.",
"jsonc": build_template("webservice", "node", "repo"),
"commands": [
"partiri auth --key <KEY>",
"partiri init --template",
"# edit .partiri.jsonc with the values above and real UUIDs from `partiri -j llm context`",
"partiri -j validate --remote",
"partiri -j -y service create",
"partiri -j -y service deploy",
]
},
{
"name": "static-site",
"description": "Pre-built static site from a public repo.",
"jsonc": build_template("static", "static", "repo"),
"commands": [
"partiri init --template",
"# set deploy_type=static, runtime=static, no run_command, build_path=dist",
"partiri -j -y service create",
"partiri -j -y service deploy",
]
},
{
"name": "private-registry-image",
"description": "Pre-built Docker image from a private registry; requires fk_service_secret.",
"jsonc": build_template("webservice", "registry", "registry"),
"commands": [
"partiri -j llm context | jq '.data.workspaces[0].registry_secrets'",
"partiri -j -y service token --secret <SECRET_UUID>",
"partiri -j validate --remote",
"partiri -j -y service create",
"partiri -j -y service deploy",
]
},
{
"name": "rust-private-service",
"description": "Internal Rust service (not exposed publicly).",
"jsonc": build_template("private-service", "rust", "repo"),
"commands": [
"partiri init --template",
"partiri -j -y service create",
"partiri -j -y service deploy",
]
}
]);
if ctx().json {
print_result(&examples);
} else {
println!("{}", serde_json::to_string_pretty(&examples).unwrap());
}
Ok(())
}
pub fn run_capabilities() -> Result<()> {
let cmd = crate::cli::Cli::command();
let tree = walk_command(&cmd);
if ctx().json {
print_result(&tree);
} else {
println!("{}", serde_json::to_string_pretty(&tree).unwrap());
}
Ok(())
}
fn walk_command(cmd: &clap::Command) -> Value {
let args: Vec<Value> = cmd
.get_arguments()
.filter(|a| !a.is_positional() || a.get_id().as_str() != "help")
.map(|a| {
let id = a.get_id().as_str();
let mut o = serde_json::Map::new();
o.insert("name".into(), Value::String(id.into()));
if let Some(s) = a.get_short() {
o.insert("short".into(), Value::String(s.to_string()));
}
if let Some(l) = a.get_long() {
o.insert("long".into(), Value::String(l.into()));
}
o.insert("required".into(), Value::Bool(a.is_required_set()));
let takes_value = a.get_action().takes_values();
o.insert("takes_value".into(), Value::Bool(takes_value));
if let Some(h) = a.get_help() {
o.insert("help".into(), Value::String(h.to_string()));
}
if takes_value {
if let Some(values) = a.get_possible_values().get(0..).filter(|v| !v.is_empty()) {
let v: Vec<Value> = values
.iter()
.map(|pv| Value::String(pv.get_name().into()))
.collect();
o.insert("possible_values".into(), Value::Array(v));
}
}
Value::Object(o)
})
.collect();
let subs: Vec<Value> = cmd.get_subcommands().map(walk_command).collect();
let mut o = serde_json::Map::new();
o.insert("name".into(), Value::String(cmd.get_name().into()));
if let Some(about) = cmd.get_about() {
o.insert("about".into(), Value::String(about.to_string()));
}
o.insert("args".into(), Value::Array(args));
if !subs.is_empty() {
o.insert("subcommands".into(), Value::Array(subs));
}
Value::Object(o)
}
pub fn run_errors() -> Result<()> {
let catalog = json!([
{ "code": "400", "meaning": "Bad request — config values out of range or wrong type",
"hint": "Check that your configuration values are valid.",
"likely_causes": ["Configuration values are out of range or wrong type"],
"suggested_commands": ["partiri validate"] },
{ "code": "401", "meaning": "Unauthorized",
"hint": "Run 'partiri auth' to update your API key.",
"likely_causes": ["API key expired or revoked", "Wrong PARTIRI_API_URL"],
"suggested_commands": ["partiri auth --key <K>", "partiri llm doctor"] },
{ "code": "402", "meaning": "Insufficient workspace balance",
"hint": "Top up at https://partiri.cloud/settings/billing",
"likely_causes": ["Workspace balance is empty"],
"suggested_commands": ["partiri llm whoami"] },
{ "code": "403", "meaning": "Permission denied or workspace limit reached",
"hint": "Your account may lack permission, or a workspace limit has been reached.",
"likely_causes": ["Account lacks permission", "Workspace limit reached"],
"suggested_commands": ["partiri llm whoami"] },
{ "code": "404", "meaning": "Resource not found",
"hint": "The resource was not found. It may have been deleted.",
"likely_causes": ["Resource was deleted", "Wrong UUID for workspace/project/region/pod"],
"suggested_commands": ["partiri llm context"] },
{ "code": "409", "meaning": "Conflict",
"hint": "A conflicting operation is in progress. Wait for it to finish, then retry.",
"likely_causes": ["Conflicting operation in progress"],
"suggested_commands": ["partiri service jobs"] },
{ "code": "422", "meaning": "Invalid request data / schema mismatch",
"hint": "The request data is invalid. Check your configuration values.",
"likely_causes": ["Invalid request data", "Schema mismatch with the API"],
"suggested_commands": ["partiri validate --remote"] },
{ "code": "429", "meaning": "Rate limit exceeded",
"hint": "Wait a moment and try again.",
"likely_causes": ["Rate limit exceeded"], "suggested_commands": [] },
{ "code": "5xx", "meaning": "Server-side error",
"hint": "Try again later, or contact support.",
"likely_causes": ["Transient backend issue"], "suggested_commands": [] },
{ "code": "auth", "meaning": "No API key configured locally",
"hint": "Configure a key via 'partiri auth --key <K>'.",
"likely_causes": ["No API key configured"],
"suggested_commands": ["partiri auth --key <K>"] },
{ "code": "validation", "meaning": "Local config validation failed",
"hint": "Fix the failing fields, then re-run.",
"likely_causes": [], "suggested_commands": ["partiri llm next"] },
{ "code": "network", "meaning": "API host unreachable",
"hint": "Check connectivity and PARTIRI_API_URL.",
"likely_causes": ["API host unreachable", "Wrong PARTIRI_API_URL"],
"suggested_commands": ["partiri llm doctor"] },
{ "code": "config", "meaning": "Bad .partiri.jsonc content",
"hint": "Re-check the file against the schema.", "likely_causes": [],
"suggested_commands": ["partiri validate"] },
{ "code": "cancelled", "meaning": "User aborted (Ctrl-C / inquire cancel) — exit code 2",
"hint": null, "likely_causes": [], "suggested_commands": [] },
{ "code": "missing_dependency", "meaning": "A required predecessor wasn't done",
"hint": null, "likely_causes": [], "suggested_commands": ["partiri llm next"] }
]);
if ctx().json {
print_result(&catalog);
} else {
println!("{}", serde_json::to_string_pretty(&catalog).unwrap());
}
Ok(())
}
pub fn run_explain(command: &str) -> Result<()> {
let cmd = crate::cli::Cli::command();
let target = match find_subcommand(&cmd, command) {
Some(c) => c,
None => {
return Err(Box::new(
CliError::new("validation", format!("Unknown command '{}'.", command))
.with_hint("Run 'partiri llm capabilities' to list every command."),
));
}
};
let pitfalls = pitfalls_for(command);
let info = json!({
"command": command,
"description": target.get_about().map(|s| s.to_string()),
"args": walk_command(target).get("args").cloned(),
"subcommands": walk_command(target).get("subcommands").cloned(),
"pitfalls": pitfalls,
});
if ctx().json {
print_result(&info);
} else {
println!("{}", serde_json::to_string_pretty(&info).unwrap());
}
Ok(())
}
fn find_subcommand<'a>(root: &'a clap::Command, path: &str) -> Option<&'a clap::Command> {
let mut cur = root;
for segment in path.split_whitespace() {
cur = cur.find_subcommand(segment)?;
}
Some(cur)
}
fn pitfalls_for(command: &str) -> Vec<&'static str> {
match command {
"init" => vec![
"Refuses to overwrite an existing .partiri.jsonc — delete it first.",
"Without --template the command requires a TTY (it runs the wizard).",
],
"auth" => vec![
"API key must be ≥64 characters; control characters are rejected.",
"From a TTY, an existing key is preserved unless --force is passed.",
],
"validate" => vec![
"Without --remote, only static/local checks run.",
"--remote needs an API key.",
],
"service create" => vec![
"Requires every fk_* field set in .partiri.jsonc; run validate --remote first.",
"service.name must be ≤16 chars and unique within the project.",
],
"service deploy" => vec![
"Destructive operation — pass -y to skip the confirmation in scripts.",
"Best-effort refresh of deploy_tag after the job is created — may still be empty if the deploy hasn't completed. Run 'partiri llm next' or 'partiri service pull' to refresh later.",
],
"service kill" => vec![
"Destructive operation — pass -y to skip the confirmation in scripts.",
],
"service pause" => vec![
"Destructive operation — pass -y to skip the confirmation in scripts.",
"Pausing stops billable compute but preserves config; resume with 'service unpause'.",
],
"service unpause" => vec![
"Destructive operation — pass -y to skip the confirmation in scripts.",
"Resumes a paused service. Idempotent if the service is already running.",
],
"service link" => vec![
"--workspace requires --project (projects don't carry across workspaces).",
"--region requires --pod.",
],
"service token" => vec![
"Lists registry secrets when registry_url is set, otherwise repository secrets.",
],
"llm context" => vec![
"One call returns workspaces + their projects/regions/pods/secrets/services.",
"Pass --workspace <UUID> to scope to one workspace and reduce work.",
],
_ => vec![],
}
}
pub fn run_whoami(client: &ApiClient) -> Result<()> {
let key_path = crate::modules::auth::credentials_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<unknown>".into());
let key_configured = crate::modules::auth::read_key().is_some();
let api_url =
std::env::var("PARTIRI_API_URL").unwrap_or_else(|_| "https://api.partiri.cloud".into());
let workspaces = client.list_workspaces()?;
let user_email = workspaces.first().and_then(|w| w.email.clone());
let payload = json!({
"key_path": key_path,
"key_configured": key_configured,
"api_url": api_url,
"user_email": user_email,
"workspace_count": workspaces.len(),
});
if ctx().json {
print_result(&payload);
} else {
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
}
Ok(())
}
pub fn run_doctor() -> Result<()> {
let mut checks: Vec<Value> = Vec::new();
let key_path = crate::modules::auth::credentials_path();
match &key_path {
Some(p) if p.exists() => checks.push(json!({
"name": "credentials_file",
"status": "ok",
"message": format!("found at {}", p.display()),
"fix": null,
})),
Some(p) => checks.push(json!({
"name": "credentials_file",
"status": "fail",
"message": format!("missing at {}", p.display()),
"fix": "partiri auth --key <KEY>",
})),
None => checks.push(json!({
"name": "credentials_file",
"status": "fail",
"message": "could not determine config directory",
"fix": "set $HOME or $XDG_CONFIG_HOME",
})),
}
let key_loaded = crate::modules::auth::read_key().is_some();
checks.push(json!({
"name": "key_readable",
"status": if key_loaded { "ok" } else { "fail" },
"message": if key_loaded { "key file readable and non-empty" } else { "key not readable or empty" },
"fix": if key_loaded { Value::Null } else { Value::String("partiri auth --key <KEY>".into()) },
}));
let mut api_ok = false;
if key_loaded {
match ApiClient::new() {
Ok(client) => match client.list_workspaces() {
Ok(ws) => {
api_ok = true;
checks.push(json!({
"name": "api_reachable",
"status": "ok",
"message": format!("authenticated; {} workspace(s) visible", ws.len()),
"fix": null,
}));
}
Err(e) => checks.push(json!({
"name": "api_reachable",
"status": "fail",
"message": format!("API call failed: {}", e),
"fix": "check PARTIRI_API_URL and that the key is valid",
})),
},
Err(e) => checks.push(json!({
"name": "api_reachable",
"status": "fail",
"message": format!("could not construct API client: {}", e),
"fix": "partiri auth --key <KEY>",
})),
}
} else {
checks.push(json!({
"name": "api_reachable",
"status": "warn",
"message": "skipped — no API key",
"fix": null,
}));
}
if Path::new(CONFIG_FILE).exists() {
match PartiriConfig::load() {
Ok(_) => checks.push(json!({
"name": "partiri_jsonc",
"status": "ok",
"message": "found and parses",
"fix": null,
})),
Err(e) => checks.push(json!({
"name": "partiri_jsonc",
"status": "fail",
"message": format!("parse error: {}", e),
"fix": "partiri llm template",
})),
}
} else {
checks.push(json!({
"name": "partiri_jsonc",
"status": "warn",
"message": format!("no {} in current directory (not required for top-level commands)", CONFIG_FILE),
"fix": "partiri init --template",
}));
}
let _ = api_ok;
let payload = json!({ "checks": checks });
let has_fail = checks.iter().any(|c| c["status"] == "fail");
if ctx().json {
print_result(&payload);
} else {
for c in &checks {
let icon = match c["status"].as_str() {
Some("ok") => "✓",
Some("warn") => "!",
Some("fail") => "✗",
_ => "?",
};
println!(
" {} {:<22} {}",
icon,
c["name"].as_str().unwrap_or(""),
c["message"].as_str().unwrap_or("")
);
if let Some(fix) = c["fix"].as_str() {
println!(" fix: {}", fix);
}
}
}
if has_fail {
return Err(Box::new(
CliError::new("validation", "Doctor reported failing checks.")
.with_hint("See the per-check 'fix' field for the recommended action."),
));
}
Ok(())
}
pub fn run_context(client: &ApiClient, workspace: Option<String>) -> Result<()> {
let workspaces = client.list_workspaces()?;
let user_email = workspaces.first().and_then(|w| w.email.clone());
let scoped: Vec<_> = match &workspace {
Some(id) => workspaces.into_iter().filter(|w| &w.id == id).collect(),
None => workspaces,
};
let mut ws_payload: Vec<Value> = Vec::new();
for w in &scoped {
let projects = client.list_projects(&w.id).unwrap_or_default();
let regions = client.list_regions(&w.id).unwrap_or_default();
let pods = client.list_pods(&w.id).unwrap_or_default();
let registry_secrets = client.list_registry_secrets(&w.id).unwrap_or_default();
let repository_secrets = client.list_repository_secrets(&w.id).unwrap_or_default();
let mut services_per_project: Vec<Value> = Vec::new();
for p in &projects {
if let Ok(svcs) = client.list_services(&p.id) {
for s in svcs {
services_per_project.push(json!({
"id": s.id,
"name": s.name,
"fk_project": p.id,
"fk_region": s.fk_region,
"fk_pod": s.fk_pod,
"deploy_type": s.deploy_type,
"runtime": s.runtime,
}));
}
}
}
ws_payload.push(json!({
"id": w.id,
"name": w.name,
"email": w.email,
"projects": projects.iter().map(|p| json!({
"id": p.id,
"name": p.name,
"environment": p.environment,
})).collect::<Vec<_>>(),
"regions": regions.iter().map(|r| json!({
"id": r.id,
"name": r.name,
"label": r.label,
"country_code": r.country_code,
})).collect::<Vec<_>>(),
"pods": pods.iter().map(|p| json!({
"id": p.id,
"name": p.name,
"label": p.label,
"cpu": p.cpu,
"ram": p.ram,
})).collect::<Vec<_>>(),
"registry_secrets": registry_secrets.iter().map(|s| json!({
"id": s.id,
"name": s.name,
"provider": s.provider,
})).collect::<Vec<_>>(),
"repository_secrets": repository_secrets.iter().map(|s| json!({
"id": s.id,
"name": s.name,
"provider": s.provider,
})).collect::<Vec<_>>(),
"services": services_per_project,
}));
}
let payload = json!({
"user": { "email": user_email },
"workspaces": ws_payload,
});
if ctx().json {
print_result(&payload);
} else {
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
}
Ok(())
}
pub fn run_next() -> Result<()> {
let key_configured = crate::modules::auth::read_key().is_some();
let jsonc_exists = Path::new(CONFIG_FILE).exists();
let (state, next_command, rationale): (String, String, String) = if !key_configured {
(
"needs_auth".into(),
"partiri auth --key <KEY>".into(),
"No API key configured. Configure one before running anything else.".into(),
)
} else if !jsonc_exists {
(
"needs_init".into(),
"partiri init --template".into(),
"No .partiri.jsonc in this directory. Write a template, then fill it in.".into(),
)
} else {
match PartiriConfig::load() {
Ok(cfg) => deduce_state(&cfg),
Err(e) => {
let payload = json!({
"state": "config_broken",
"next_command": "partiri validate",
"rationale": format!("Existing .partiri.jsonc fails to parse: {}. Fix the file.", e),
});
if ctx().json {
print_result(&payload);
} else {
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
}
return Ok(());
}
}
};
let payload = json!({
"state": state,
"next_command": next_command,
"rationale": rationale,
});
if ctx().json {
print_result(&payload);
} else {
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
}
Ok(())
}
fn deduce_state(cfg: &PartiriConfig) -> (String, String, String) {
if cfg.fk_workspace.is_empty()
|| cfg.fk_project.is_empty()
|| cfg.service.fk_region.is_empty()
|| cfg.service.fk_pod.is_empty()
{
return (
"needs_uuids".into(),
"partiri -j llm context".into(),
"One or more fk_* fields are empty. Fetch the UUIDs and edit the file.".into(),
);
}
if cfg.id.is_none() {
return (
"needs_create".into(),
"partiri -j validate --remote && partiri -j -y service create".into(),
"Config looks ready. Validate against the API, then register the service.".into(),
);
}
if cfg.deploy_tag.is_some() {
return (
"deployed".into(),
"partiri -j service jobs".into(),
"Service is deployed. Inspect jobs/logs/metrics from here.".into(),
);
}
if let (Some(id), Ok(client)) = (cfg.id.as_deref(), ApiClient::new()) {
if let Ok(jobs) = client.list_service_jobs(id) {
let mut deploys: Vec<_> = jobs
.into_iter()
.filter(|j| j.job_type == "deploy")
.collect();
deploys.sort_by(|a, b| b.created_at.cmp(&a.created_at));
if let Some(latest) = deploys.first() {
match latest.status.as_str() {
"succeeded" => {
return (
"deployed".into(),
"partiri service pull".into(),
"A deploy job succeeded but deploy_tag is not yet set in .partiri.jsonc — refresh from the API.".into(),
);
}
"in_progress" | "open" => {
return (
"deploying".into(),
"partiri -j service jobs".into(),
"A deploy job is in progress. Watch the job status.".into(),
);
}
"failed" | "timed_out" => {
return (
"deploy_failed".into(),
"partiri -j service jobs".into(),
format!(
"The most recent deploy job ended with status '{}'. Inspect jobs/logs.",
latest.status
),
);
}
_ => {}
}
}
}
}
(
"needs_deploy".into(),
"partiri -j -y service deploy".into(),
"Service is registered but never deployed. Trigger a deploy.".into(),
)
}