Files
vdm/cli/src/main.cpp
T
samiandClaude Sonnet 5 0c7ce1437c cli: velox — add / ls / pause / resume / rm, with --json (build step 8, pulled forward)
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
2026-09-10 15:27:20 +04:00

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);
}