douyin-cli 2026.8.23

A Rust CLI for Douyin OpenAPI and web workflows
Documentation
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

use serde_json::{json, Value};

pub fn manifest(version: &str, config_file: &Path) -> Value {
    json!({
        "name": "douyin-cli",
        "version": version,
        "homepage": "https://github.com/LIghtJUNction/douyin",
        "entrypoint": "douyin",
        "configFile": config_file,
        "auth": {
            "type": "oauth2",
            "login": ["douyin", "auth", "login"],
            "exchangeCode": ["douyin", "auth", "code", "--code", "<code>"],
            "status": ["douyin", "auth", "status", "--json"],
            "refresh": ["douyin", "auth", "refresh"],
            "logout": ["douyin", "auth", "logout"]
        },
        "openapi": {
            "output": "json",
            "tokenSource": "saved-oauth-config-or-env",
            "commands": {
                "userinfo": ["douyin", "api", "userinfo"],
                "commentList": ["douyin", "api", "comment-list", "--item-id", "<item_id>"],
                "commentReplies": ["douyin", "api", "comment-replies", "--item-id", "<item_id>", "--comment-id", "<comment_id>"],
                "commentReply": ["douyin", "api", "comment-reply", "--item-id", "<item_id>", "--comment-id", "<comment_id>", "--content", "<content>", "--yes"],
                "imMessageSend": ["douyin", "api", "im-message-send", "--to-user-id", "<to_user_id>", "--msg-id", "<msg_id>", "--conversation-id", "<conversation_id>", "--text", "<text>", "--yes"],
                "request": ["douyin", "api", "request", "<method>", "<path>"]
            }
        },
        "environment": {
            "clientKey": "DOUYIN_CLIENT_KEY",
            "clientSecret": "DOUYIN_CLIENT_SECRET",
            "accessToken": "DOUYIN_ACCESS_TOKEN",
            "home": "DOUYIN_HOME"
        }
    })
}

pub fn status(binary: &str) -> Value {
    match find_in_path(binary) {
        Some(path) => {
            let version = Command::new(&path)
                .arg("--version")
                .output()
                .ok()
                .map(|output| {
                    let bytes = if output.stdout.is_empty() {
                        output.stderr
                    } else {
                        output.stdout
                    };
                    String::from_utf8_lossy(&bytes).trim().to_owned()
                })
                .filter(|value| !value.is_empty());
            json!({"available": true, "binary": binary, "path": path, "version": version})
        }
        None => json!({"available": false, "binary": binary, "path": null, "version": null}),
    }
}

fn find_in_path(binary: &str) -> Option<PathBuf> {
    let direct = PathBuf::from(binary);
    if direct.components().count() > 1 && direct.is_file() {
        return Some(direct);
    }
    env::split_paths(&env::var_os("PATH")?).find_map(|directory| {
        let candidate = directory.join(binary);
        candidate.is_file().then_some(candidate)
    })
}