The scriptable client, built now rather than last: it is how the daemon gets exercised before the GUI is pointed at it (AGENT-DAEMON.md). - src/client — synchronous blocking RPC over the Unix socket: resolve $XDG_RUNTIME_DIR/velox/velox.sock, connect, session.hello, one framed request/reply per call. Distinguishes transport failure (exit 3) from a daemon-returned error (exit 1). - src/main — subcommands add/ls/pause/resume/rm; --json prints the raw JSON-RPC result or error; --dir/--out/--segments on add; --delete-file on rm. Usage errors exit 2. ls works end to end against veloxd today (empty table). add and the bulk verbs reach the daemon and surface its "not implemented" (-32603) cleanly until the store lands — the plumbing is done, the commands light up as handlers do. Test velox.client: the real Client against an in-process UdsServer — no-daemon path, session.hello, download.list, and a not-implemented method surfacing as an RPC error rather than a transport error. ASan+UBSan clean; full tree (21 tests, incl. conformance) green. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
180 lines
5.6 KiB
C++
180 lines
5.6 KiB
C++
// velox — the command-line client for veloxd.
|
|
//
|
|
// velox add <url> [--dir D] [--out NAME] [--segments N] [--json]
|
|
// velox ls [--json]
|
|
// velox pause <id>... velox resume <id>...
|
|
// velox rm <id>... [--delete-file]
|
|
//
|
|
// Exit codes: 0 ok, 1 daemon returned an error, 2 usage error, 3 cannot reach the daemon.
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "client.hpp"
|
|
|
|
namespace {
|
|
|
|
using nlohmann::json;
|
|
using velox::cli::CallError;
|
|
using velox::cli::Client;
|
|
|
|
constexpr int kOk = 0;
|
|
constexpr int kRpcError = 1;
|
|
constexpr int kUsage = 2;
|
|
constexpr int kNoDaemon = 3;
|
|
|
|
struct Args {
|
|
std::vector<std::string> positional;
|
|
bool json = false;
|
|
bool delete_file = false;
|
|
std::string dir;
|
|
std::string out;
|
|
long segments = 0;
|
|
};
|
|
|
|
[[noreturn]] void usage(int code) {
|
|
std::fprintf(code == kOk ? stdout : stderr,
|
|
"usage: velox <command> [options]\n\n"
|
|
" add <url> [--dir DIR] [--out NAME] [--segments N]\n"
|
|
" ls\n"
|
|
" pause <id>...\n"
|
|
" resume <id>...\n"
|
|
" rm <id>... [--delete-file]\n\n"
|
|
" --json print the raw JSON-RPC result\n");
|
|
std::exit(code);
|
|
}
|
|
|
|
Args parse_args(int argc, char** argv) {
|
|
Args a;
|
|
for (int i = 2; i < argc; ++i) {
|
|
const std::string_view arg = argv[i];
|
|
if (arg == "--json") {
|
|
a.json = true;
|
|
} else if (arg == "--delete-file") {
|
|
a.delete_file = true;
|
|
} else if (arg == "--dir" && i + 1 < argc) {
|
|
a.dir = argv[++i];
|
|
} else if (arg == "--out" && i + 1 < argc) {
|
|
a.out = argv[++i];
|
|
} else if (arg == "--segments" && i + 1 < argc) {
|
|
a.segments = std::strtol(argv[++i], nullptr, 10);
|
|
} else if (arg == "-h" || arg == "--help") {
|
|
usage(kOk);
|
|
} else if (!arg.empty() && arg.front() == '-') {
|
|
std::fprintf(stderr, "velox: unknown option %s\n", argv[i]);
|
|
usage(kUsage);
|
|
} else {
|
|
a.positional.emplace_back(arg);
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
int report_error(const CallError& e, bool as_json) {
|
|
if (as_json) {
|
|
json j = {{"error", {{"code", e.code}, {"message", e.message}}}};
|
|
if (!e.data.is_null()) j["error"]["data"] = e.data;
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
} else {
|
|
std::fprintf(stderr, "velox: %s\n", e.message.c_str());
|
|
}
|
|
return e.code == CallError::kConnect ? kNoDaemon : kRpcError;
|
|
}
|
|
|
|
void print_task_table(const json& items) {
|
|
std::printf("%-38s %-10s %-9s %s\n", "ID", "STATE", "PROGRESS", "NAME");
|
|
for (const auto& t : items) {
|
|
const std::string id = t.value("taskId", "");
|
|
const std::string state = t.value("state", "");
|
|
const std::string name = t.value("filename", "");
|
|
const long long total = t.value("sizeBytes", 0LL);
|
|
const long long done = t.value("downloadedBytes", 0LL);
|
|
char pct[12] = "-";
|
|
if (total > 0) std::snprintf(pct, sizeof(pct), "%lld%%", done * 100 / total);
|
|
std::printf("%-38s %-10s %-9s %s\n", id.c_str(), state.c_str(), pct, name.c_str());
|
|
}
|
|
}
|
|
|
|
int cmd_ls(Client& c, const Args& a) {
|
|
auto r = c.call("download.list", json::object());
|
|
if (!r) return report_error(r.error(), a.json);
|
|
if (a.json) {
|
|
std::printf("%s\n", r->dump(2).c_str());
|
|
return kOk;
|
|
}
|
|
const auto& items = r->contains("items") ? r->at("items") : json::array();
|
|
if (items.empty()) {
|
|
std::printf("no downloads\n");
|
|
return kOk;
|
|
}
|
|
print_task_table(items);
|
|
return kOk;
|
|
}
|
|
|
|
int cmd_add(Client& c, const Args& a) {
|
|
if (a.positional.empty()) {
|
|
std::fprintf(stderr, "velox add: a URL is required\n");
|
|
return kUsage;
|
|
}
|
|
json params = {{"url", a.positional.front()}};
|
|
if (!a.dir.empty()) params["saveDir"] = a.dir;
|
|
if (!a.out.empty()) params["filename"] = a.out;
|
|
if (a.segments > 0) params["segments"] = a.segments;
|
|
|
|
auto r = c.call("download.add", params);
|
|
if (!r) return report_error(r.error(), a.json);
|
|
if (a.json) {
|
|
std::printf("%s\n", r->dump(2).c_str());
|
|
} else {
|
|
std::printf("added %s\n", r->value("taskId", "?").c_str());
|
|
}
|
|
return kOk;
|
|
}
|
|
|
|
int cmd_bulk(Client& c, const Args& a, const char* method) {
|
|
if (a.positional.empty()) {
|
|
std::fprintf(stderr, "velox: at least one task id is required\n");
|
|
return kUsage;
|
|
}
|
|
json params = {{"taskIds", a.positional}};
|
|
if (std::string_view(method) == "download.remove" && a.delete_file) params["deleteFile"] = true;
|
|
|
|
auto r = c.call(method, params);
|
|
if (!r) return report_error(r.error(), a.json);
|
|
if (a.json) {
|
|
std::printf("%s\n", r->dump(2).c_str());
|
|
} else {
|
|
std::printf("ok\n");
|
|
}
|
|
return kOk;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 2) usage(kUsage);
|
|
const std::string command = argv[1];
|
|
if (command == "-h" || command == "--help") usage(kOk);
|
|
|
|
const Args args = parse_args(argc, argv);
|
|
|
|
Client client;
|
|
if (const auto err = client.connect()) {
|
|
return report_error(*err, args.json);
|
|
}
|
|
|
|
if (command == "ls") return cmd_ls(client, args);
|
|
if (command == "add") return cmd_add(client, args);
|
|
if (command == "pause") return cmd_bulk(client, args, "download.pause");
|
|
if (command == "resume") return cmd_bulk(client, args, "download.resume");
|
|
if (command == "rm") return cmd_bulk(client, args, "download.remove");
|
|
|
|
std::fprintf(stderr, "velox: unknown command '%s'\n", command.c_str());
|
|
usage(kUsage);
|
|
}
|