feat(cli): crafter-debug, the client agents drive an app with

Cli::Run is the whole client but its transport, so tests run it in
process against a real Session; main will hand it the QUIC connection.

- Commands: ls, status, cmds, help, call, get, set, pause, resume,
  step, wait, trace, log (with --follow), screenshot, quit, debugger,
  guide and version, with -i, --json and --timeout.
- call types key=value from the command's argument schema (slot=1 is
  a number, name=1 a string, at.x=3 nests), so agents need no JSON
  for simple calls; a value that cannot fit its field is a usage
  error before anything is sent.
- The instance comes from discovery; one running instance needs no -i.
  The record's protocol is checked before any request: a mismatch
  exits 4, says which side is newer and prints the app's own client.
  Other sources only warn.
- A request with no reply is diagnosed: the app has exited, stopped
  in a debugger (with the attach command), or no longer calls Pump
  (with what to change). Exit codes: 2 usage, 3 no instance,
  4 version, 5 timeout, 6 the app's error.
- Output is short by default, arrays cut at 20 with a note; --json
  prints everything. `guide` is the full reference for agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 02:03:38 +01:00
commit 34c60dcd34
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
7 changed files with 1359 additions and 10 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,26 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Spike placeholder for the crafter-debug client: reports where it runs.
// crafter-debug: Cli::Run with the real transport.
import std;
namespace fs = std::filesystem;
import Crafter.Debug.Core;
import Crafter.Debug.Cli;
using namespace Crafter::Debug;
namespace {
// Until the QUIC transport lands: every call reports it unreachable.
class Unbuilt : public Cli::Connection {
public:
std::expected<Value, Cli::CallError> Call(const Value&, std::chrono::milliseconds, const std::function<bool(const Value&)>&) override {
return std::unexpected(Cli::CallError{ Cli::CallError::Kind::Unreachable, "this client was built without its transport" });
}
};
}
int main(int argc, char** argv) {
std::println("crafter-debug spike client at {}", fs::read_symlink("/proc/self/exe").string());
std::println("inside a Flatpak sandbox: {}", fs::exists("/.flatpak-info"));
for (std::int32_t i = 1; i < argc; ++i) std::println("arg: {}", std::string_view(argv[i]));
return 0;
const std::vector<std::string_view> args(argv + 1, argv + argc);
Cli::Environment environment;
environment.connect = [](const InstanceRecord&) -> std::unique_ptr<Cli::Connection> { return std::make_unique<Unbuilt>(); };
environment.haveLldb = [] { return std::filesystem::exists("/app/bin/lldb") || std::filesystem::exists("/usr/bin/lldb"); };
return Cli::Run(args, environment);
}