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

View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// crafter-debug, the client agents drive an app with. Run is the whole
// program but for the transport: main hands it a Connector that reaches an
// instance over QUIC, and tests hand it one that reaches a Session in the
// same process.
//
// Exit codes:
// 0 done
// 2 a usage error, or no debug build yet (the launcher's stub)
// 3 no instance to talk to
// 4 the app and this client speak different protocols
// 5 timed out
// 6 the app answered with an error
// 1 is left to the launcher and `flatpak run` failing.
export module Crafter.Debug.Cli:Client;
import std;
import Crafter.Debug.Core;
export namespace Crafter::Debug::Cli {
inline constexpr std::int32_t exitOk = 0;
inline constexpr std::int32_t exitUsage = 2;
inline constexpr std::int32_t exitNoInstance = 3;
inline constexpr std::int32_t exitVersion = 4;
inline constexpr std::int32_t exitTimeout = 5;
inline constexpr std::int32_t exitError = 6;
struct CallError {
enum class Kind : std::uint8_t {
// No connection: the app is gone, or its port does not answer.
Unreachable,
// Connected, but no reply within the timeout.
TimedOut,
// The reply was not what the protocol says.
Broken,
};
Kind kind = Kind::Unreachable;
std::string message;
};
// One request to an instance. The reply is the last object it sends;
// objects before it are stream lines (`log --follow`), handed to
// `onLine`, which returns false to stop.
class Connection {
public:
virtual ~Connection() = default;
virtual std::expected<Value, CallError> Call(const Value& request, std::chrono::milliseconds timeout,
const std::function<bool(const Value& line)>& onLine) = 0;
};
using Connector = std::function<std::unique_ptr<Connection>(const InstanceRecord& instance)>;
struct Environment {
Connector connect;
std::function<std::vector<Instance>()> list = ListInstances;
std::ostream* out = &std::cout;
std::ostream* err = &std::cerr;
// Where a screenshot without -o goes, relative paths included.
std::filesystem::path cwd = std::filesystem::current_path();
// Whether `lldb` exists here, for the attach command `debugger` prints.
std::function<bool()> haveLldb;
};
std::int32_t Run(std::span<const std::string_view> args, Environment& environment);
// The full reference an agent reads with `crafter-debug guide`.
std::string_view Guide();
// "30s", "500ms", "2m", "1h"; a bare number is seconds.
std::optional<std::chrono::milliseconds> ParseDuration(std::string_view text);
}