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
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user