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

@ -1,11 +1,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Three configurations:
// Four configurations:
// Crafter.Debug.Core what the app and the client share (values, codecs,
// the protocol, discovery, build info)
// Crafter.Debug.Cli the client's code, a library so tests can run it
// crafter.debug-cli the client, crafter-debug
// Crafter.Debug what an app links (returned), depending on both
// Crafter.Debug what an app links (returned), depending on the core
// and the client
// The client is one of the library's dependencies only so that a consumer's
// debug build compiles it in the same pass as the app; an executable adds no
// link flags to what depends on it. Consumers load this file through
@ -135,7 +137,20 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
client.mtune = cfg.mtune;
client.sysroot = cfg.sysroot;
client.debug = cfg.debug;
client.dependencies = { &core };
// The client's code as a library, so tests can run it in process.
Configuration& cli = owned.emplace_back();
cli.path = Root();
cli.name = "Crafter.Debug.Cli";
cli.outputName = "Crafter.Debug.Cli";
cli.type = ConfigurationType::LibraryStatic;
cli.target = cfg.target;
cli.march = cfg.march;
cli.mtune = cfg.mtune;
cli.sysroot = cfg.sysroot;
cli.debug = cfg.debug;
cli.dependencies = { &core };
client.dependencies = { &cli };
{
std::array<fs::path, 8> interfaces = {
@ -157,6 +172,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
};
core.GetInterfacesAndImplementations(interfaces, implementations);
}
{
std::array<fs::path, 2> interfaces = { "interfaces/Crafter.Debug.Cli-Client", "interfaces/Crafter.Debug.Cli" };
std::array<fs::path, 1> implementations = { "implementations/Crafter.Debug.Cli-Client" };
cli.GetInterfacesAndImplementations(interfaces, implementations);
}
{
std::array<fs::path, 0> interfaces = {};
std::array<fs::path, 1> implementations = { "implementations/main" };
@ -186,6 +206,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
if (IsTopLevelProject()) {
for (const std::string_view test : coreTests) cfg.AddTest(test).Dependencies({ &core });
for (const std::string_view test : libraryTests) cfg.AddTest(test).Dependencies({ &cfg });
cfg.AddTest("ShouldRunClientCommands").Dependencies({ &cfg, &cli });
}
return cfg;
}