fix(cli): take global flags after the command, and call --json again

Only --json was taken after the command, so `wait --paused --timeout
60s`, the order the guide's own synopsis and agents use, failed with
"unknown argument '--timeout'", and `-i` after the command did too.
And since that --json was taken as the output flag, `call X --json
'{...}'`, the documented way to pass arguments as JSON, never reached
call: its object then failed as "not key=value".

--timeout, -i and --json now go anywhere on the line; no command has
options of those names. After `call`, a --json followed by an object
is the arguments, and any other --json the output flag. A trailing
--timeout or -i without its value is a usage error that says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 03:21:41 +01:00
commit 7574ad5c4e
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 23 additions and 4 deletions

View file

@ -878,14 +878,20 @@ namespace {
for (std::size_t i = 0; i < args.size(); ++i) { for (std::size_t i = 0; i < args.size(); ++i) {
const std::string_view arg = args[i]; const std::string_view arg = args[i];
if (!options.command.empty()) { if (!options.command.empty()) {
// Global flags may follow the command too, before its own. // Global flags go anywhere, as agents write them: `wait
if (arg == "--json") { // --paused --timeout 60s`. No command has options of these
// names, but for `call --json '{...}'`, its arguments as JSON.
const bool callArguments = options.command == "call" && arg == "--json" && i + 1 < args.size() && args[i + 1].starts_with('{');
if (arg == "--json" && !callArguments) {
options.json = true; options.json = true;
continue; continue;
} }
if (arg != "--timeout" && arg != "-i" && arg != "--instance") {
options.rest.emplace_back(arg); options.rest.emplace_back(arg);
if (callArguments) options.rest.emplace_back(args[++i]);
continue; continue;
} }
}
if ((arg == "-i" || arg == "--instance") && i + 1 < args.size()) { if ((arg == "-i" || arg == "--instance") && i + 1 < args.size()) {
options.instance = args[++i]; options.instance = args[++i];
} else if (arg == "--json") { } else if (arg == "--json") {
@ -897,6 +903,9 @@ namespace {
return exitUsage; return exitUsage;
} }
options.timeout = *timeout; options.timeout = *timeout;
} else if (arg == "--timeout" || arg == "-i" || arg == "--instance") {
*env.err << std::format("crafter-debug: {} needs a value\n", arg);
return exitUsage;
} else if (arg == "-h" || arg == "--help") { } else if (arg == "-h" || arg == "--help") {
*env.out << usage; *env.out << usage;
return exitOk; return exitOk;

View file

@ -161,6 +161,10 @@ int main() {
Check(r.code == 0 && Has(r.out, "'score >= 1' held at frame"), "wait for a condition"); Check(r.code == 0 && Has(r.out, "'score >= 1' held at frame"), "wait for a condition");
r = Run({ "--timeout", "100ms", "wait", "score < 0" }, instances); r = Run({ "--timeout", "100ms", "wait", "score < 0" }, instances);
Check(r.code == 5 && Has(r.err, "was not true within"), "a wait that times out exits 5 and says what the paths were"); Check(r.code == 5 && Has(r.err, "was not true within"), "a wait that times out exits 5 and says what the paths were");
r = Run({ "wait", "score < 0", "--timeout", "100ms" }, instances);
Check(r.code == 5 && Has(r.err, "was not true within"), std::format("global flags also go after the command's own ({})", r.err));
r = Run({ "wait", "--frames", "1", "--timeout" }, instances);
Check(r.code == 2 && Has(r.err, "--timeout needs a value"), "a global flag without its value is a usage error");
r = Run({ "log", "--last", "5" }, instances); r = Run({ "log", "--last", "5" }, instances);
Check(r.code == 0 && Has(r.out, "AGENT"), "log shows the agent's actions"); Check(r.code == 0 && Has(r.out, "AGENT"), "log shows the agent's actions");
@ -178,6 +182,12 @@ int main() {
} catch (const ParseError&) { } catch (const ParseError&) {
} }
Check(r.code == 0 && json, "--json prints JSON"); Check(r.code == 0 && json, "--json prints JSON");
r = Run({ "status", "--json" }, instances);
Check(r.code == 0 && r.out.starts_with("{"), "--json after the command too");
r = Run({ "call", "Player_SetAim", "--json", R"({"slot": 4, "yaw": 5})" }, instances);
Check(r.code == 0 && Has(r.out, R"({"slot":4,"yaw":5.0})"), std::format("call takes its arguments as JSON ({})", r.out + r.err));
r = Run({ "call", "Player_SetAim", "--json", R"({"slot": 4})", "yaw=7", "--json" }, instances);
Check(r.code == 0 && Has(r.out, R"({"slot":4,"yaw":7.0})"), std::format("JSON arguments with key=value on top, and --json after them ({})", r.out + r.err));
r = Run({ "stauts" }, instances); r = Run({ "stauts" }, instances);
Check(r.code == 2 && Has(r.err, "did you mean 'status'"), "a mistyped command name gets a suggestion"); Check(r.code == 2 && Has(r.err, "did you mean 'status'"), "a mistyped command name gets a suggestion");