qq-cli 0.1.0

A modular Rust QQ task interface for coding agents
use crate::{
    core::error::{AppError, Result},
    native::job::JobId,
    storage::{
        references::References,
        state::{Store, atomic_json, private_dir, reject_links},
    },
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(super) struct Target {
    pub instance_id: String,
    pub run_id: String,
    pub installation_id: String,
    pub installation_digest: String,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum Outcome {
    Pending,
    Completed,
    Unknown,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(super) struct Record {
    pub schema_version: u8,
    pub target: Target,
    pub job: Option<JobId>,
    pub outcome: Outcome,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Pointer {
    schema_version: u8,
    reference: String,
}
fn path(store: &Store, target: &Target) -> Result<PathBuf> {
    for key in [&target.instance_id, &target.run_id, &target.installation_id] {
        crate::core::domain::request_key(key).map_err(|_| AppError::storage())?;
    }
    if target.installation_digest.len() != 64
        || !target
            .installation_digest
            .bytes()
            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
    {
        return Err(AppError::storage());
    }
    let digest = Sha256::digest(
        serde_json::to_vec(&(&target.instance_id, &target.run_id))
            .map_err(|_| AppError::storage())?,
    );
    let path = store
        .root()
        .join("instance-stops")
        .join(format!("{digest:x}.json"));
    reject_links(&path)?;
    Ok(path)
}
pub(super) fn load(store: &Store, refs: &References, target: &Target) -> Result<Option<Record>> {
    let path = path(store, target)?;
    let pointer: Pointer = match crate::storage::config::read_json(&path, 4096) {
        Ok(p) => p,
        Err(e) if e.code == "FILE_NOT_FOUND" => return Ok(None),
        Err(e) => return Err(e),
    };
    if pointer.schema_version != 1 {
        return Err(AppError::storage());
    }
    let record: Record = refs.get("p1", &pointer.reference)?;
    if record.schema_version != 1 || record.target != *target {
        return Err(AppError::storage());
    }
    if let Some(job) = &record.job {
        job.validate()?;
    }
    Ok(Some(record))
}
pub(super) fn save(store: &Store, refs: &References, record: &Record) -> Result<()> {
    let path = path(store, &record.target)?;
    private_dir(path.parent().ok_or_else(AppError::storage)?)?;
    atomic_json(
        &path,
        &Pointer {
            schema_version: 1,
            reference: refs.issue("p1", record)?,
        },
    )
}