Agent Sandbox Registry

agent-sandbox

Writing abx command modules

无中文版

src/cli/commands/README.md

Writing abx command modules

This directory holds the command modules of the CLI (sandbox.ts, sandbox_files.ts, sandbox_process.ts, sandbox_system.ts, template.ts, config_cmd.ts, mcp.ts, deploy.ts, ...). The framework they build on is a byte-for-byte port of Click 8.5.0 (src/framework/); the output layer is a port of cli/output.py (src/cli/output.ts). Everything below is the contract between those layers and a command module.

1. Export convention (how main.ts finds your commands)

src/cli/main.ts registers command modules lazily. A registry entry is a spec string "commands/<module>:<attr>" (module path relative to src/cli, no extension). When the root group needs the command (help listing, resolution, invocation) it runs import("./commands/<module>.js") and reads the export called <attr>.

The registry is static code in src/cli/command_surface.ts: GROUP_COMMANDS (the command groups) and SHORTCUTS (the fixed top-level shortcuts). It is not configurable; tests/test_cli/test_command_surface.test.ts checks it against abx --help and against docs/en/reference/cli-commands.md. A module exports each click command/group under the attribute name used in its spec (these are data keys, do not rename them to camelCase):

spec the module must export
commands/sandbox:sandbox export const sandbox = group("sandbox", ...)
commands/config_cmd:config export const config = group("config", ...)
commands/mcp:mcp export const mcp = group("mcp", ...)
commands/template:template export const template = group("template", ...)
commands/provider:provider export const provider = group("provider", ...)
commands/sandbox:create / list_cmd / info / kill / exec_cmd / connect / upload / download / run_cmd the shortcut targets (abx create, abx list, ...)
commands/deploy:deploy_shortcut idem

Rules:

2. Framework API cheat-sheet (src/framework/index.ts)

There are no decorators; every factory returns the object.

import {
  command, group, option, argument, versionOption, helpOption,   // factories
  Context, Command, Group, Choice, IntRange, FloatRange, Path, Tuple, INT, FLOAT, BOOL, STRING, UUID, UNPROCESSED,
  UsageError, BadParameter, MissingParameter, ClickException, Abort, Exit,
  echo, secho, style, prompt, confirm, getCurrentContext, sys, sysExit, SystemExit,
} from "../../framework/index.js";
import { getOutput, enableVerbose } from "../output.js";
import { getFormatter } from "../formatters.js";
import { regionOption, resolveRegion } from "../region.js";
import { handleErrors, EXIT_AUTH } from "../main.js";

export const create = command("create", {
  help: "Create a new sandbox.\n\n\b\nExamples:\n  abx create --template base",   // Python docstring, cleandoc'd, \b kept
  params: [
    argument(["description"], { required: false }),
    option(["--template", "-t"], { default: null, help: "Template name" }),
    option(["--timeout"], { type: INT, default: null }),
    option(["--verbose", "-v"], { is_flag: true }),
    regionOption(),                                                // shared --region/-r
  ],
  callback: handleErrors(async (ctx, params) => {                   // params: { description, template, timeout, verbose, region }
    const out = getOutput(ctx);
    if (params.verbose) enableVerbose(ctx);
    out.info("Creating sandbox...");
    out.data({ ID: "sbx-1", Status: "running" });
  }),
});

3. Output (src/cli/output.ts)

getOutput(ctx?) returns the OutputManager the root group stored in ctx.meta["abx.output"] (falls back to a default manager when called outside a context). Channel policy: stdout = results (data, table, success), stderr = everything else (info, progress, warning, error, debug, and every logging record).

out.info("..."); out.progress("..."); out.warning("..."); out.debug("...");
out.error("boom", { code: "E1001", suggestion: "retry" });
out.success("done");
out.data(new Map([["ID", "sbx-1"], ["Status", "running"]]));   // object or Map (Map keeps key order for integer-like keys)
out.table(["ID", "Status"], [["sbx-1", "running"]]);
await out.spinner("Creating sandbox", () => doIt());            // context-manager -> callback
await out.activity("Researching", async (update) => { update?.("line1\nline2"); }, { tick: true });
const act = out.startActivity("Building", { tick: true });     // imperative form: { update | null, close() }
try { act.update?.("..."); } finally { act.close(); }

jsonMode / quiet / noColor / verbose / ci are plain fields (json_mode / no_color getters exist as Python-spelling aliases). Constructor options accept both spellings (new OutputManager({ jsonMode: true }) / { json_mode: true }).

getFormatter(ctx) (formatters.ts) gives the legacy OutputFormatter (printSuccess, printError(msg, { code, suggestion }), printTable, printDict, printData). handleErrors uses it with ctx.obj.

Test seams (Python module-attribute monkeypatches) are on the mutable outputSeams object (sys, isTty, echo, Console, activityMinInterval, activityTickSeconds, monotonic).

4. Testing a command

import { CliRunner } from "../helpers/cliRunner.js";
import { cli } from "../../src/cli/main.js";
const runner = new CliRunner();
const result = await runner.invoke(cli, ["create", "--template", "base"]);
expect(result.exit_code).toBe(0);
expect(result.stdout).toContain("created successfully");   // stdout / stderr / output (interleaved) like Click 8.5

CliRunner.invoke(cmd, args, { input, env, catch_exceptions, color, prog_name }) - isolated env, buffered in-memory non-tty streams, forced terminal width 80, ANSI stripped unless color: true. tests/test_cli/conftest.ts provides the shared fixtures (runner, pristineLogging, defaultShortcutsOnly, capsys).