@autifyhq/aximo-cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +24 -1
  2. package/dist/commands/device/device-config.d.ts +2 -2
  3. package/dist/commands/device/device-config.js +5 -8
  4. package/dist/commands/device/index.js +2 -1
  5. package/dist/commands/device/paths.d.ts +4 -0
  6. package/dist/commands/device/paths.js +12 -0
  7. package/dist/commands/device/reset.d.ts +1 -0
  8. package/dist/commands/device/reset.js +18 -3
  9. package/dist/commands/device/reset.test.js +13 -0
  10. package/dist/commands/device/testbox/box.d.ts +90 -0
  11. package/dist/commands/device/testbox/box.js +379 -0
  12. package/dist/commands/device/testbox/box.test.d.ts +1 -0
  13. package/dist/commands/device/testbox/box.test.js +815 -0
  14. package/dist/commands/device/testbox/container.d.ts +58 -0
  15. package/dist/commands/device/testbox/container.js +289 -0
  16. package/dist/commands/device/testbox/docker.d.ts +31 -0
  17. package/dist/commands/device/testbox/docker.js +78 -0
  18. package/dist/commands/device/testbox/docker.test.d.ts +1 -0
  19. package/dist/commands/device/testbox/docker.test.js +82 -0
  20. package/dist/commands/device/testbox/down.d.ts +7 -0
  21. package/dist/commands/device/testbox/down.js +14 -0
  22. package/dist/commands/device/testbox/errors.d.ts +4 -0
  23. package/dist/commands/device/testbox/errors.js +7 -0
  24. package/dist/commands/device/testbox/fake-docker.d.ts +25 -0
  25. package/dist/commands/device/testbox/fake-docker.js +35 -0
  26. package/dist/commands/device/testbox/index.d.ts +2 -0
  27. package/dist/commands/device/testbox/index.js +16 -0
  28. package/dist/commands/device/testbox/logs.d.ts +15 -0
  29. package/dist/commands/device/testbox/logs.js +43 -0
  30. package/dist/commands/device/testbox/prune.d.ts +11 -0
  31. package/dist/commands/device/testbox/prune.js +17 -0
  32. package/dist/commands/device/testbox/registration.d.ts +9 -0
  33. package/dist/commands/device/testbox/registration.js +56 -0
  34. package/dist/commands/device/testbox/reset.d.ts +7 -0
  35. package/dist/commands/device/testbox/reset.js +14 -0
  36. package/dist/commands/device/testbox/result.d.ts +8 -0
  37. package/dist/commands/device/testbox/result.js +23 -0
  38. package/dist/commands/device/testbox/result.test.d.ts +1 -0
  39. package/dist/commands/device/testbox/result.test.js +34 -0
  40. package/dist/commands/device/testbox/self-command.d.ts +12 -0
  41. package/dist/commands/device/testbox/self-command.js +36 -0
  42. package/dist/commands/device/testbox/self-command.test.d.ts +1 -0
  43. package/dist/commands/device/testbox/self-command.test.js +58 -0
  44. package/dist/commands/device/testbox/status.d.ts +7 -0
  45. package/dist/commands/device/testbox/status.js +11 -0
  46. package/dist/commands/device/testbox/up.d.ts +47 -0
  47. package/dist/commands/device/testbox/up.js +76 -0
  48. package/dist/commands/device/testbox/up.test.d.ts +1 -0
  49. package/dist/commands/device/testbox/up.test.js +52 -0
  50. package/dist/commands/device/testbox/url.d.ts +7 -0
  51. package/dist/commands/device/testbox/url.js +20 -0
  52. package/dist/commands/device/user-devices.d.ts +4 -0
  53. package/dist/commands/device/user-devices.js +18 -0
  54. package/dist/commands/mcp/index.js +6 -3
  55. package/dist/commands/mcp/server.d.ts +2 -2
  56. package/dist/commands/mcp/server.test.js +28 -1
  57. package/dist/commands/mcp/testbox-tools.test.d.ts +1 -0
  58. package/dist/commands/mcp/testbox-tools.test.js +278 -0
  59. package/dist/commands/mcp/tools.d.ts +8 -1
  60. package/dist/commands/mcp/tools.js +252 -19
  61. package/dist/commands/mcp/tools.test.js +151 -3
  62. package/dist/commands/mcp/wait.d.ts +20 -0
  63. package/dist/commands/mcp/wait.js +46 -0
  64. package/dist/commands/mcp/wait.test.d.ts +1 -0
  65. package/dist/commands/mcp/wait.test.js +91 -0
  66. package/dist/commands/schema.js +4 -0
  67. package/dist/commands/session/cancel.d.ts +20 -0
  68. package/dist/commands/session/cancel.js +39 -0
  69. package/dist/commands/session/export.d.ts +30 -0
  70. package/dist/commands/session/export.js +58 -0
  71. package/dist/commands/session/index.js +2 -0
  72. package/dist/commands/session/session.test.js +100 -0
  73. package/dist/errors.d.ts +2 -0
  74. package/dist/errors.js +5 -0
  75. package/dist/errors.test.js +6 -1
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.js +1 -0
  78. package/dist/session-export.d.ts +30 -0
  79. package/dist/session-export.js +72 -0
  80. package/dist/session-export.test.d.ts +1 -0
  81. package/dist/session-export.test.js +107 -0
  82. package/dist/types.d.ts +5 -0
  83. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,8 @@ Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` r
73
73
  | `aximo project list` | List organization projects |
74
74
  | `aximo session create` | Create and queue a test session |
75
75
  | `aximo session get` | Get session status |
76
+ | `aximo session cancel` | Cancel a queued or running session |
77
+ | `aximo session export` | Download a session result as a PDF or a screenshot ZIP |
76
78
  | `aximo session save-as-case` | Save a finished session as a reusable test case |
77
79
  | `aximo case create` | Create a test case |
78
80
  | `aximo case update` | Update a test case |
@@ -105,6 +107,7 @@ Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` r
105
107
  | `aximo device logs` | Show the BYOD agent log |
106
108
  | `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
107
109
  | `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
110
+ | `aximo device testbox` | Sandboxed Chromium in Docker as a local device (up/down/...) |
108
111
  | `aximo mobile list` | List physically-connected phones (Android adb, iOS devicectl) |
109
112
  | `aximo mobile setup` | Install the Appium drivers used to drive phones |
110
113
  | `aximo mobile doctor` | Diagnose prerequisites (adb/Xcode/devicectl, signing, driver) |
@@ -145,10 +148,16 @@ aximo session create --title <t> --prompt <p> --project-id <id> \
145
148
  [--json '<body>'] [--dry-run]
146
149
 
147
150
  aximo session get <sessionId>
151
+
152
+ aximo session cancel <sessionId> [--dry-run]
153
+
154
+ aximo session export <sessionId> [--format pdf|screenshots] [--output <path>] [--locale en|ja|ko]
148
155
  ```
149
156
 
150
157
  Desktop sessions default to `platform=desktop`, `model=sonnet`. Mobile sessions require both `--device-arn` and `--upload-arn`.
151
158
 
159
+ `session export` writes the test result report (`pdf`, the default) or a ZIP of the step screenshots with `metadata.csv` and `session.csv` (`screenshots`) to `--output`, defaulting to `<sessionId>.pdf` / `<sessionId>-screenshots.zip`. The download is verified complete before it replaces the output file.
160
+
152
161
  ### Test cases
153
162
 
154
163
  ```bash
@@ -355,6 +364,20 @@ aximo device unregister --device-id DEVICE_ID # unregister an orphaned device (
355
364
 
356
365
  `device ping` returns what the server knows, including `lastSeenAgoSeconds` and the HTTP round-trip `latencyMs`. It does **not** round-trip through the relay, so a passing ping tells you "the server thinks this device is online," not "the local agent is responding right now."
357
366
 
367
+ ### The testbox: a local device that is not your screen
368
+
369
+ Registering your laptop hands Aximo your mouse and keyboard for the whole run. The testbox is the alternative for apps on `localhost`: one Chromium on a virtual display inside a Docker container, registered as its own local device, with a live view you can watch (and type a password into) from a browser tab. Requires Docker; macOS and Linux (Windows untested).
370
+
371
+ ```bash
372
+ aximo device testbox up --ports 3000 # pull once, register, start, connect; prints the live-view link
373
+ aximo device testbox status # container, agent, device online?, which ports answer
374
+ aximo device testbox url # the live-view link again (password included)
375
+ aximo device testbox down # stop; keeps the browser profile and the registration
376
+ aximo device testbox prune # remove everything, including the device on the server
377
+ ```
378
+
379
+ Use the printed `userDeviceId` as the local device for `session create`, cases and plans, and put the `http://localhost:<port>` URL in the prompt. `--block-hosts prod.example.com` makes the browser refuse production; it is a browser rule matched by name, not a network boundary, and the box can reach whatever your machine can. `--name qa` runs a second box as a second device. The API key never enters the container: the host CLI registers the box and injects only the device token over stdin. MCP agents drive the same box through `aximo_start_testbox` / `aximo_get_testbox` / `aximo_stop_testbox`.
380
+
358
381
  ### Re-registering
359
382
 
360
383
  `aximo device register` refuses to overwrite an existing local registration. To move a registration to a different device name:
@@ -670,7 +693,7 @@ claude mcp add aximo -- aximo mcp
670
693
 
671
694
  VS Code (Copilot agent mode) uses the same entry under a `servers` key in `.vscode/mcp.json`.
672
695
 
673
- The server exposes 29 tools: 17 read-only ones for inspecting sessions, cases, plans, plan runs, devices, and uploads (`aximo_get_session_timeline` is the load-bearing one for Playwright export; `aximo_list_plans` and `aximo_get_plan` cover plan discovery; `aximo_list_plan_runs` and `aximo_get_plan_run` cover run-status polling; `aximo_list_devices`, `aximo_list_user_devices`, and `aximo_list_uploads` supply the device and upload tokens that case configs and run overrides take), 10 that author content (create, update, archive, and delete cases — including a case's per-target device configuration — create, update, and archive plans, save a session as a case), and 2 that trigger runs (`aximo_run_case` starts a case run and returns the `sessionId`; `aximo_run_plan` starts a plan run and returns the `planRunId` — both spend organization credits and require the case or plan to be `apiEnabled`). See the [MCP reference](./skills/aximo-cli/references/mcp.md) for per-client config paths, the full tool table, the Playwright export prompt, and a no-client smoke test.
696
+ The server exposes 40 tools: 18 read-only ones for inspecting sessions, cases, plans, plan runs, devices, and uploads (`aximo_get_session_timeline` is the load-bearing one for Playwright export; `aximo_wait_for_session` polls a run; `aximo_list_plans` and `aximo_get_plan` cover plan discovery; `aximo_list_plan_runs` and `aximo_get_plan_run` cover run-status polling; `aximo_list_devices`, `aximo_list_user_devices`, and `aximo_list_uploads` supply the device and upload tokens that case configs and run overrides take), 2 that export a session result to a local file (`aximo_export_session_pdf` for the test result report, `aximo_export_session_screenshots` for a ZIP of the step screenshots), 13 that author content (create, update, archive, restore, and delete cases — including a case's per-target device configuration — create, update, archive, restore, and delete plans, add or remove a plan's cases, save a session as a case), 3 that start runs (`aximo_run_case` starts a case run and returns the `sessionId`; `aximo_create_session` starts a session from a prompt with no saved case; `aximo_run_plan` starts a plan run and returns the `planRunId` — all spend organization credits, and the case and plan runs require `apiEnabled`), 1 that stops one (`aximo_cancel_session` cancels a queued or running session), and 3 that manage the local testbox (`aximo_start_testbox`, `aximo_get_testbox`, `aximo_stop_testbox`: a sandboxed Chromium in Docker, registered as a local device, so an agent can test an app on localhost without taking over your screen; see `aximo device testbox`). See the [MCP reference](./skills/aximo-cli/references/mcp.md) for per-client config paths, the full tool table, the Playwright export prompt, and a no-client smoke test.
674
697
 
675
698
  ## Agent Skills
676
699
 
@@ -9,6 +9,6 @@ export interface DeviceConfig {
9
9
  * Read and validate the local device registration (~/.aximo/device.json).
10
10
  * Returns null when the file is missing, unreadable, or malformed.
11
11
  */
12
- export declare function readDeviceConfig(): DeviceConfig | null;
12
+ export declare function readDeviceConfig(path?: string): DeviceConfig | null;
13
13
  /** Persist the local device registration with owner-only permissions. */
14
- export declare function writeDeviceConfig(config: DeviceConfig): void;
14
+ export declare function writeDeviceConfig(config: DeviceConfig, path?: string): void;
@@ -1,11 +1,11 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { getAximoDir, getDeviceConfigPath } from "./paths.js";
2
+ import { dirname } from "node:path";
3
+ import { getDeviceConfigPath } from "./paths.js";
3
4
  /**
4
5
  * Read and validate the local device registration (~/.aximo/device.json).
5
6
  * Returns null when the file is missing, unreadable, or malformed.
6
7
  */
7
- export function readDeviceConfig() {
8
- const path = getDeviceConfigPath();
8
+ export function readDeviceConfig(path = getDeviceConfigPath()) {
9
9
  if (!existsSync(path))
10
10
  return null;
11
11
  try {
@@ -31,11 +31,8 @@ export function readDeviceConfig() {
31
31
  }
32
32
  }
33
33
  /** Persist the local device registration with owner-only permissions. */
34
- export function writeDeviceConfig(config) {
35
- const dir = getAximoDir();
36
- if (!existsSync(dir))
37
- mkdirSync(dir, { recursive: true });
38
- const path = getDeviceConfigPath();
34
+ export function writeDeviceConfig(config, path = getDeviceConfigPath()) {
35
+ mkdirSync(dirname(path), { recursive: true });
39
36
  writeFileSync(path, JSON.stringify(config, null, 2), { mode: 0o600 });
40
37
  try {
41
38
  chmodSync(path, 0o600);
@@ -2,7 +2,7 @@ import { defineCommand } from "citty";
2
2
  export default defineCommand({
3
3
  meta: {
4
4
  name: "device",
5
- description: "BYOD device commands (register/connect/status/...); 'device list' lists Device Farm mobile devices",
5
+ description: "BYOD device commands (register/connect/status/...); 'device testbox' runs a sandboxed browser in Docker; 'device list' lists Device Farm mobile devices",
6
6
  },
7
7
  subCommands: {
8
8
  list: () => import("./list.js").then((m) => m.default),
@@ -15,5 +15,6 @@ export default defineCommand({
15
15
  logs: () => import("./logs.js").then((m) => m.default),
16
16
  reset: () => import("./reset.js").then((m) => m.default),
17
17
  ping: () => import("./ping.js").then((m) => m.default),
18
+ testbox: () => import("./testbox/index.js").then((m) => m.default),
18
19
  },
19
20
  });
@@ -10,4 +10,8 @@ export declare function getAgentDir(): string;
10
10
  export declare function getAgentScriptPath(): string;
11
11
  export declare function getAgentRequirementsPath(): string;
12
12
  export declare function getAgentManifestPath(): string;
13
+ export declare function getTestboxDir(): string;
14
+ export declare function getTestboxConfigPath(name: string): string;
15
+ export declare function getTestboxStatePath(name: string): string;
16
+ export declare function getTestboxLogPath(name: string): string;
13
17
  export declare function getEmbeddedPythonBinary(): string;
@@ -36,6 +36,18 @@ export function getAgentRequirementsPath() {
36
36
  export function getAgentManifestPath() {
37
37
  return join(getAgentDir(), "manifest.json");
38
38
  }
39
+ export function getTestboxDir() {
40
+ return join(getAximoDir(), "testbox");
41
+ }
42
+ export function getTestboxConfigPath(name) {
43
+ return join(getTestboxDir(), `${name}.json`);
44
+ }
45
+ export function getTestboxStatePath(name) {
46
+ return join(getTestboxDir(), `${name}.state.json`);
47
+ }
48
+ export function getTestboxLogPath(name) {
49
+ return join(getTestboxDir(), `${name}.log`);
50
+ }
39
51
  export function getEmbeddedPythonBinary() {
40
52
  return platform() === "win32"
41
53
  ? join(getPythonDir(), "python.exe")
@@ -1,6 +1,7 @@
1
1
  import type { Envelope } from "../../types.js";
2
2
  export interface ResetResult {
3
3
  removed: string[];
4
+ orphanedTestboxes?: string[];
4
5
  }
5
6
  export interface RunDeviceResetOptions {
6
7
  python?: boolean;
@@ -1,13 +1,21 @@
1
1
  import { defineCommand } from "citty";
2
- import { existsSync, rmSync } from "node:fs";
2
+ import { existsSync, readdirSync, rmSync } from "node:fs";
3
3
  import { formatError, formatSuccess, printResult } from "../../output.js";
4
- import { getAximoDir, getCacheDir, getDepsMarkerPath, getDeviceConfigPath, getPythonDir, getPythonMarkerPath, getRemoteLogPath, } from "./paths.js";
4
+ import { getAximoDir, getCacheDir, getDepsMarkerPath, getDeviceConfigPath, getPythonDir, getPythonMarkerPath, getRemoteLogPath, getTestboxDir, } from "./paths.js";
5
5
  function removeIfExists(path, removed, opts = {}) {
6
6
  if (!existsSync(path))
7
7
  return;
8
8
  rmSync(path, { recursive: opts.recursive ?? false, force: true });
9
9
  removed.push(path);
10
10
  }
11
+ function listTestboxes() {
12
+ const dir = getTestboxDir();
13
+ if (!existsSync(dir))
14
+ return [];
15
+ return readdirSync(dir)
16
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".state.json"))
17
+ .map((f) => f.slice(0, -".json".length));
18
+ }
11
19
  export function runDeviceReset(options = {}) {
12
20
  const { python, deps, device, all } = options;
13
21
  if (!python && !deps && !device && !all) {
@@ -16,8 +24,12 @@ export function runDeviceReset(options = {}) {
16
24
  const removed = [];
17
25
  try {
18
26
  if (all) {
27
+ const orphanedTestboxes = listTestboxes();
19
28
  removeIfExists(getAximoDir(), removed, { recursive: true });
20
- return formatSuccess({ removed });
29
+ return formatSuccess({
30
+ removed,
31
+ ...(orphanedTestboxes.length > 0 ? { orphanedTestboxes } : {}),
32
+ });
21
33
  }
22
34
  if (python) {
23
35
  removeIfExists(getPythonDir(), removed, { recursive: true });
@@ -60,6 +72,9 @@ export default defineCommand({
60
72
  all: Boolean(args.all),
61
73
  });
62
74
  printResult(result);
75
+ if (result.ok && result.data.orphanedTestboxes) {
76
+ console.error(`\nNote: testbox registrations for ${result.data.orphanedTestboxes.join(", ")} were removed locally only; their device rows remain on the server. Delete them in Settings > Devices, or run \`aximo device testbox prune\` before \`reset --all\` next time.`);
77
+ }
63
78
  if (result.ok && args.device) {
64
79
  console.error("\nNote: this only removed local credentials. The device row on the Aximo server still exists —\nrun `aximo device unregister` first if you want to delete it server-side.");
65
80
  }
@@ -65,5 +65,18 @@ describe("runDeviceReset", () => {
65
65
  const result = runDeviceReset({ all: true });
66
66
  expect(result.ok).toBe(true);
67
67
  expect(existsSync(tempHome)).toBe(false);
68
+ if (result.ok)
69
+ expect(result.data.orphanedTestboxes).toBeUndefined();
70
+ });
71
+ it("--all names the testbox registrations it leaves orphaned on the server", () => {
72
+ seedAll();
73
+ mkdirSync(join(tempHome, "testbox"), { recursive: true });
74
+ writeFileSync(join(tempHome, "testbox", "default.json"), "{}");
75
+ writeFileSync(join(tempHome, "testbox", "qa.json"), "{}");
76
+ writeFileSync(join(tempHome, "testbox", "qa.state.json"), "{}");
77
+ const result = runDeviceReset({ all: true });
78
+ expect(result.ok).toBe(true);
79
+ if (result.ok)
80
+ expect(result.data.orphanedTestboxes).toEqual(["default", "qa"]);
68
81
  });
69
82
  });
@@ -0,0 +1,90 @@
1
+ import { type ClientDeps } from "../../../client.js";
2
+ import { type WaitClock } from "../../mcp/wait.js";
3
+ import { type TestboxConfig } from "./container.js";
4
+ import { type DockerRunner } from "./docker.js";
5
+ export interface TestboxDeps extends ClientDeps {
6
+ docker?: DockerRunner;
7
+ clock?: WaitClock;
8
+ fetchAgent?: (deps: ClientDeps) => Promise<{
9
+ scriptPath: string;
10
+ requirementsPath: string;
11
+ }>;
12
+ hostName?: () => string;
13
+ }
14
+ export interface UpOptions {
15
+ name?: string;
16
+ ports?: string;
17
+ blockHosts?: string[];
18
+ startUrl?: string;
19
+ novncPort?: number;
20
+ screen?: string;
21
+ image?: string;
22
+ wait?: boolean;
23
+ }
24
+ export type TestboxState = "down" | "pulling" | "starting" | "connecting" | "online" | "error";
25
+ export interface TestboxStatus {
26
+ name: string;
27
+ state: TestboxState;
28
+ container: "running" | "stopped" | "absent";
29
+ config?: TestboxConfig;
30
+ device?: {
31
+ userDeviceId: string;
32
+ name: string;
33
+ status: string;
34
+ };
35
+ agent?: "running" | "stopped";
36
+ agentStatus?: string;
37
+ ports?: {
38
+ port: number;
39
+ reachable: boolean;
40
+ }[];
41
+ liveViewUrl?: string;
42
+ lastError?: string;
43
+ }
44
+ interface StateFile {
45
+ phase: "pulling" | "starting" | "connecting" | "error";
46
+ startedAt: string;
47
+ pid: number;
48
+ lastError?: string;
49
+ }
50
+ export declare function readState(name: string): StateFile | null;
51
+ export declare function clearState(name: string): void;
52
+ /** The detached `up` still working on this box, if any. */
53
+ export declare function pendingUp(name: string, clock?: WaitClock): StateFile | null;
54
+ export interface UpResult {
55
+ name: string;
56
+ container: string;
57
+ userDeviceId: string;
58
+ deviceName: string;
59
+ state: TestboxState;
60
+ config: TestboxConfig;
61
+ liveViewUrl?: string;
62
+ notes: string[];
63
+ }
64
+ export declare function upTestbox(options: UpOptions, deps?: TestboxDeps): Promise<UpResult>;
65
+ export declare function statusTestbox(nameInput: string | undefined, deps?: TestboxDeps): Promise<TestboxStatus>;
66
+ /** Status now, or once the box is online or has stopped trying, whichever comes first. */
67
+ export declare function waitForTestbox(name: string | undefined, timeoutSeconds: number, deps?: TestboxDeps): Promise<TestboxStatus>;
68
+ export declare function downTestbox(nameInput: string | undefined, deps?: TestboxDeps): Promise<{
69
+ name: string;
70
+ removed: boolean;
71
+ }>;
72
+ export declare function resetTestbox(nameInput: string | undefined, deps?: TestboxDeps): Promise<{
73
+ profileDropped: boolean;
74
+ name: string;
75
+ removed: boolean;
76
+ }>;
77
+ export declare function pruneTestbox(nameInput: string | undefined, options?: {
78
+ keepImage?: boolean;
79
+ }, deps?: TestboxDeps): Promise<{
80
+ name: string;
81
+ removed: boolean;
82
+ deviceId: string | undefined;
83
+ serverDeleted: boolean;
84
+ }>;
85
+ export declare function agentLog(nameInput: string | undefined, lines: number, deps?: TestboxDeps): Promise<{
86
+ name: string;
87
+ lines: string[];
88
+ }>;
89
+ export declare function agentLogFollowArgs(name: string | undefined, lines: number): string[];
90
+ export {};