@autifyhq/aximo-cli 0.1.0 → 0.3.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.
- package/README.md +87 -50
- package/dist/cli.js +15 -0
- package/dist/commands/case/case.test.d.ts +1 -0
- package/dist/commands/case/case.test.js +86 -0
- package/dist/commands/case/create.d.ts +53 -0
- package/dist/commands/case/create.js +71 -0
- package/dist/commands/case/index.d.ts +2 -0
- package/dist/commands/case/index.js +7 -0
- package/dist/commands/device/index.js +4 -1
- package/dist/commands/device/list.js +4 -1
- package/dist/commands/device/register.js +1 -1
- package/dist/commands/device/status.js +4 -1
- package/dist/commands/mcp/index.d.ts +2 -0
- package/dist/commands/mcp/index.js +29 -0
- package/dist/commands/mcp/server.d.ts +3 -0
- package/dist/commands/mcp/server.js +19 -0
- package/dist/commands/mcp/server.test.d.ts +1 -0
- package/dist/commands/mcp/server.test.js +89 -0
- package/dist/commands/mcp/tools.d.ts +18 -0
- package/dist/commands/mcp/tools.js +268 -0
- package/dist/commands/mcp/tools.test.d.ts +1 -0
- package/dist/commands/mcp/tools.test.js +269 -0
- package/dist/commands/plan/add-case.d.ts +30 -0
- package/dist/commands/plan/add-case.js +58 -0
- package/dist/commands/plan/create.d.ts +31 -0
- package/dist/commands/plan/create.js +56 -0
- package/dist/commands/plan/index.d.ts +2 -0
- package/dist/commands/plan/index.js +9 -0
- package/dist/commands/plan/plan.test.d.ts +1 -0
- package/dist/commands/plan/plan.test.js +104 -0
- package/dist/commands/plan/remove-case.d.ts +26 -0
- package/dist/commands/plan/remove-case.js +43 -0
- package/dist/commands/schema.js +42 -3
- package/dist/commands/session/index.js +1 -0
- package/dist/commands/session/save-as-case.d.ts +43 -0
- package/dist/commands/session/save-as-case.js +69 -0
- package/dist/commands/session/save-as-case.test.d.ts +1 -0
- package/dist/commands/session/save-as-case.test.js +58 -0
- package/dist/index.d.ts +12 -7
- package/dist/index.js +12 -7
- package/dist/types.d.ts +147 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -36,11 +36,7 @@ Get your API key from **Settings → API Keys** in the [Aximo dashboard](https:/
|
|
|
36
36
|
|
|
37
37
|
## Configuration
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
1. Value in `$AXIMO_HOME/config.json` (written by `aximo configure`)
|
|
42
|
-
2. Environment variable
|
|
43
|
-
3. Default (for base URL only: `https://aximo.autify.com`)
|
|
39
|
+
Each credential resolves independently: the value in `$AXIMO_HOME/config.json` (written by `aximo configure`) wins, then the environment variable, then the default.
|
|
44
40
|
|
|
45
41
|
| Variable | Required | Default | Notes |
|
|
46
42
|
| ---------------- | -------- | -------------------------- | ------------------------------------------------------------ |
|
|
@@ -48,60 +44,59 @@ The CLI resolves each credential independently in this order:
|
|
|
48
44
|
| `AXIMO_BASE_URL` | No | `https://aximo.autify.com` | Override for sandbox / self-hosted |
|
|
49
45
|
| `AXIMO_HOME` | No | `~/.aximo` | Data directory (config, device credentials, embedded Python) |
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
Set them interactively or pass flags — config is written to `$AXIMO_HOME/config.json` (mode `0600` where the OS supports it; Windows falls back to file ACLs), and an empty prompt keeps the existing value:
|
|
52
48
|
|
|
53
49
|
```bash
|
|
54
50
|
aximo configure # interactive prompts
|
|
55
51
|
aximo configure --base-url URL --api-key KEY # non-interactive
|
|
56
52
|
```
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### Using environment variables
|
|
61
|
-
|
|
62
|
-
If you'd rather keep credentials out of a file (CI, ephemeral shells), set env vars:
|
|
54
|
+
Or keep credentials out of a file (CI, ephemeral shells) with env vars:
|
|
63
55
|
|
|
64
56
|
```bash
|
|
65
|
-
# macOS / Linux
|
|
66
57
|
export AXIMO_API_KEY=your-api-key
|
|
67
58
|
export AXIMO_BASE_URL=https://aximo-sbx.autify.dev # optional, prod is the default
|
|
68
|
-
|
|
69
|
-
# Windows (PowerShell, persistent)
|
|
70
|
-
[Environment]::SetEnvironmentVariable("AXIMO_API_KEY", "your-api-key", "User")
|
|
71
|
-
[Environment]::SetEnvironmentVariable("AXIMO_BASE_URL", "https://aximo-sbx.autify.dev", "User")
|
|
72
59
|
```
|
|
73
60
|
|
|
61
|
+
Credentials and device registrations are **per-environment** — to switch between production and sandbox (`https://aximo-sbx.autify.dev`), re-run `aximo configure` with the other `--base-url`.
|
|
62
|
+
|
|
74
63
|
## Commands
|
|
75
64
|
|
|
76
65
|
Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` returns machine-readable argument introspection (e.g. `aximo schema session.create`).
|
|
77
66
|
|
|
78
|
-
| Command
|
|
79
|
-
|
|
|
80
|
-
| `aximo configure`
|
|
81
|
-
| `aximo health check`
|
|
82
|
-
| `aximo project list`
|
|
83
|
-
| `aximo session create`
|
|
84
|
-
| `aximo session get`
|
|
85
|
-
| `aximo
|
|
86
|
-
| `aximo
|
|
87
|
-
| `aximo
|
|
88
|
-
| `aximo
|
|
89
|
-
| `aximo
|
|
90
|
-
| `aximo
|
|
91
|
-
| `aximo
|
|
92
|
-
| `aximo
|
|
93
|
-
| `aximo
|
|
94
|
-
| `aximo
|
|
95
|
-
| `aximo
|
|
96
|
-
| `aximo
|
|
97
|
-
| `aximo
|
|
98
|
-
| `aximo
|
|
99
|
-
| `aximo device
|
|
100
|
-
| `aximo device
|
|
101
|
-
| `aximo device
|
|
102
|
-
| `aximo device
|
|
103
|
-
| `aximo device
|
|
104
|
-
| `aximo
|
|
67
|
+
| Command | Description |
|
|
68
|
+
| ---------------------------- | ---------------------------------------------------- |
|
|
69
|
+
| `aximo configure` | Save credentials to `$AXIMO_HOME/config.json` |
|
|
70
|
+
| `aximo health check` | Check API reachability |
|
|
71
|
+
| `aximo project list` | List organization projects |
|
|
72
|
+
| `aximo session create` | Create and queue a test session |
|
|
73
|
+
| `aximo session get` | Get session status |
|
|
74
|
+
| `aximo session save-as-case` | Save a finished session as a reusable test case |
|
|
75
|
+
| `aximo case create` | Create a test case |
|
|
76
|
+
| `aximo plan create` | Create a test plan |
|
|
77
|
+
| `aximo plan add-case` | Add a case to a plan |
|
|
78
|
+
| `aximo plan remove-case` | Remove a case from a plan |
|
|
79
|
+
| `aximo trigger case run` | Trigger a dashboard-created test case |
|
|
80
|
+
| `aximo trigger case get` | Get triggered case status |
|
|
81
|
+
| `aximo trigger plan run` | Trigger a test plan |
|
|
82
|
+
| `aximo trigger plan get` | Get plan run status |
|
|
83
|
+
| `aximo upload create` | Create an upload slot for an APK/IPA |
|
|
84
|
+
| `aximo upload list` | List previously uploaded apps |
|
|
85
|
+
| `aximo upload get` | Poll upload processing status |
|
|
86
|
+
| `aximo upload put` | Upload binary file to the slot |
|
|
87
|
+
| `aximo upload submit` | Finalize upload and trigger processing |
|
|
88
|
+
| `aximo device list` | List mobile devices from Device Farm |
|
|
89
|
+
| `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
|
|
90
|
+
| `aximo device register` | Register this machine as a BYOD device |
|
|
91
|
+
| `aximo device connect` | Start the BYOD agent and connect to the relay |
|
|
92
|
+
| `aximo device status` | List your organization's BYOD devices |
|
|
93
|
+
| `aximo device ping` | Check your BYOD device's server-side status |
|
|
94
|
+
| `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
|
|
95
|
+
| `aximo device logs` | Show the BYOD agent log |
|
|
96
|
+
| `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
|
|
97
|
+
| `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
|
|
98
|
+
| `aximo schema` | Show CLI surface as JSON |
|
|
99
|
+
| `aximo mcp` | Run a stdio MCP server for agents (see below) |
|
|
105
100
|
|
|
106
101
|
Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
|
|
107
102
|
|
|
@@ -137,6 +132,32 @@ aximo session get <sessionId>
|
|
|
137
132
|
|
|
138
133
|
Desktop sessions default to `platform=desktop`, `model=sonnet`. Mobile sessions require both `--device-arn` and `--upload-arn`.
|
|
139
134
|
|
|
135
|
+
### Test cases
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
aximo case create --project-id <id> --scenario <text> \
|
|
139
|
+
[--type web|mobile|desktop] [--title <t>] [--description <d>] \
|
|
140
|
+
[--format gherkin|markdown|plain_text] [--start-url <url>] \
|
|
141
|
+
[--directory-id <id>] [--json '<body>'] [--dry-run]
|
|
142
|
+
|
|
143
|
+
aximo session save-as-case <sessionId> \
|
|
144
|
+
[--title <t>] [--type web|mobile|desktop] \
|
|
145
|
+
[--scenario <text>] [--format gherkin|markdown|plain_text] \
|
|
146
|
+
[--json '<body>'] [--dry-run]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`case create` returns a `caseId` you can run with `aximo trigger case run`. `session save-as-case` persists a finished session (terminal status, not yet linked to a case) as a reusable case, carrying over its scenario and run config. Cases created either way have API access enabled by default, so they are triggerable immediately.
|
|
150
|
+
|
|
151
|
+
### Test plans
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
aximo plan create --project-id <id> --title <t> [--description <d>] [--json '<body>'] [--dry-run]
|
|
155
|
+
aximo plan add-case <planId> --case-id <caseId> [--json '<body>'] [--dry-run]
|
|
156
|
+
aximo plan remove-case <planId> <caseId> [--dry-run]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Build a runnable plan: `plan create` → `plan add-case` (repeat per case) → `aximo trigger plan run`. Plans created this way have API access enabled by default. Cases must belong to the same workspace as the plan.
|
|
160
|
+
|
|
140
161
|
### Triggers (test cases and plans)
|
|
141
162
|
|
|
142
163
|
```bash
|
|
@@ -146,7 +167,7 @@ aximo trigger plan run <planId> [...]
|
|
|
146
167
|
aximo trigger plan get <planId> <planRunId>
|
|
147
168
|
```
|
|
148
169
|
|
|
149
|
-
Triggers
|
|
170
|
+
Triggers a test case or plan. API access must be enabled on the resource — on by default for cases/plans created via this CLI; for dashboard-created ones, enable it in the Aximo dashboard settings. `trigger case run` returns a `sessionId` you can poll; `trigger plan run` returns a per-case breakdown.
|
|
150
171
|
|
|
151
172
|
### Uploads
|
|
152
173
|
|
|
@@ -335,17 +356,33 @@ The agent disables DPI-awareness and uses a calibrated scroll amount — if you
|
|
|
335
356
|
|
|
336
357
|
Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Windows. Override via the `AXIMO_HOME` env var.
|
|
337
358
|
|
|
338
|
-
|
|
359
|
+
> Devices are registered per-environment (see [Configuration](#configuration)) — a device registered against sandbox can't connect to production and vice versa.
|
|
360
|
+
|
|
361
|
+
## MCP server
|
|
362
|
+
|
|
363
|
+
`aximo mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, exposing your recorded sessions, screenshots, and test cases to MCP-capable agents (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, …). The motivating use: let an agent read a finished session and generate a Playwright spec without you piping JSON between commands.
|
|
364
|
+
|
|
365
|
+
It reuses the same credentials as every other command, so run `aximo configure` (or set `AXIMO_API_KEY`) first.
|
|
366
|
+
|
|
367
|
+
**Claude Code** — one command:
|
|
339
368
|
|
|
340
369
|
```bash
|
|
341
|
-
|
|
342
|
-
|
|
370
|
+
claude mcp add aximo -- aximo mcp
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Other clients** — point them at the `aximo mcp` stdio command. Claude Desktop, Cursor, and Windsurf use the `mcpServers` key:
|
|
343
374
|
|
|
344
|
-
|
|
345
|
-
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"mcpServers": {
|
|
378
|
+
"aximo": { "command": "aximo", "args": ["mcp"] }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
346
381
|
```
|
|
347
382
|
|
|
348
|
-
|
|
383
|
+
VS Code (Copilot agent mode) uses the same entry under a `servers` key in `.vscode/mcp.json`.
|
|
384
|
+
|
|
385
|
+
The server exposes 15 tools: 10 read-only ones for inspecting sessions and cases (`aximo_get_session_timeline` is the load-bearing one for Playwright export) plus 5 that author content (create cases and plans, save a session as a case). 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.
|
|
349
386
|
|
|
350
387
|
## Agent Skills
|
|
351
388
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
3
|
import pkg from "../package.json" with { type: "json" };
|
|
4
|
+
// citty's --help prints to stdout; route to stderr for `mcp` since its stdout
|
|
5
|
+
// is JSON-RPC.
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args[0] === "mcp" && args.some((a) => a === "--help" || a === "-h")) {
|
|
8
|
+
process.stderr.write(`aximo mcp — stdio MCP server exposing Aximo data plus create operations.\n` +
|
|
9
|
+
`\n` +
|
|
10
|
+
`Wire into an MCP client (e.g. Claude Desktop, Claude Code) by pointing it\n` +
|
|
11
|
+
`at this binary. Auth uses ~/.aximo/config.json (run \`aximo configure\`).\n` +
|
|
12
|
+
`\n` +
|
|
13
|
+
`Stdout is reserved for JSON-RPC messages — diagnostics go to stderr.\n`);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
4
16
|
const main = defineCommand({
|
|
5
17
|
meta: {
|
|
6
18
|
name: "aximo",
|
|
@@ -11,11 +23,14 @@ const main = defineCommand({
|
|
|
11
23
|
configure: () => import("./commands/configure.js").then((m) => m.default),
|
|
12
24
|
health: () => import("./commands/health/index.js").then((m) => m.default),
|
|
13
25
|
session: () => import("./commands/session/index.js").then((m) => m.default),
|
|
26
|
+
case: () => import("./commands/case/index.js").then((m) => m.default),
|
|
27
|
+
plan: () => import("./commands/plan/index.js").then((m) => m.default),
|
|
14
28
|
trigger: () => import("./commands/trigger/index.js").then((m) => m.default),
|
|
15
29
|
device: () => import("./commands/device/index.js").then((m) => m.default),
|
|
16
30
|
upload: () => import("./commands/upload/index.js").then((m) => m.default),
|
|
17
31
|
project: () => import("./commands/project/index.js").then((m) => m.default),
|
|
18
32
|
schema: () => import("./commands/schema.js").then((m) => m.default),
|
|
33
|
+
mcp: () => import("./commands/mcp/index.js").then((m) => m.default),
|
|
19
34
|
},
|
|
20
35
|
});
|
|
21
36
|
runMain(main);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runCaseCreate } from "./create.js";
|
|
4
|
+
beforeAll(() => {
|
|
5
|
+
process.env["AXIMO_API_KEY"] = "test-key";
|
|
6
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
7
|
+
server.listen();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => server.resetHandlers());
|
|
10
|
+
afterAll(() => server.close());
|
|
11
|
+
const MIN = {
|
|
12
|
+
projectId: "proj-1",
|
|
13
|
+
scenario: "Open the login page and verify it loads",
|
|
14
|
+
};
|
|
15
|
+
describe("runCaseCreate", () => {
|
|
16
|
+
it("POSTs body and returns success envelope", async () => {
|
|
17
|
+
let seen;
|
|
18
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, async ({ request: req }) => {
|
|
19
|
+
seen = await req.json();
|
|
20
|
+
return HttpResponse.json({ success: true, caseId: "c1" }, { status: 201 });
|
|
21
|
+
}));
|
|
22
|
+
const result = await runCaseCreate({ ...MIN, type: "web", title: "Login" });
|
|
23
|
+
expect(result.ok).toBe(true);
|
|
24
|
+
if (result.ok && !("dryRun" in result.data))
|
|
25
|
+
expect(result.data.caseId).toBe("c1");
|
|
26
|
+
expect(seen).toMatchObject({
|
|
27
|
+
projectId: "proj-1",
|
|
28
|
+
scenario: "Open the login page and verify it loads",
|
|
29
|
+
type: "web",
|
|
30
|
+
title: "Login",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it("rejects missing projectId", async () => {
|
|
34
|
+
const result = await runCaseCreate({ scenario: "x" });
|
|
35
|
+
expect(result.ok).toBe(false);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
expect(result.error.code).toBe("validation_error");
|
|
38
|
+
});
|
|
39
|
+
it("rejects missing scenario", async () => {
|
|
40
|
+
const result = await runCaseCreate({ projectId: "p1" });
|
|
41
|
+
expect(result.ok).toBe(false);
|
|
42
|
+
if (!result.ok)
|
|
43
|
+
expect(result.error.code).toBe("validation_error");
|
|
44
|
+
});
|
|
45
|
+
it("--json replaces individual flags and POSTs raw body", async () => {
|
|
46
|
+
let seen;
|
|
47
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, async ({ request: req }) => {
|
|
48
|
+
seen = await req.json();
|
|
49
|
+
return HttpResponse.json({ success: true, caseId: "c2" }, { status: 201 });
|
|
50
|
+
}));
|
|
51
|
+
const raw = JSON.stringify({ projectId: "p1", scenario: "raw", extra: true });
|
|
52
|
+
const result = await runCaseCreate({ json: raw });
|
|
53
|
+
expect(result.ok).toBe(true);
|
|
54
|
+
expect(seen).toEqual({ projectId: "p1", scenario: "raw", extra: true });
|
|
55
|
+
});
|
|
56
|
+
it("--dry-run returns request without calling API", async () => {
|
|
57
|
+
const result = await runCaseCreate({ ...MIN, dryRun: true });
|
|
58
|
+
expect(result.ok).toBe(true);
|
|
59
|
+
if (result.ok && "dryRun" in result.data) {
|
|
60
|
+
expect(result.data.dryRun).toBe(true);
|
|
61
|
+
expect(result.data.method).toBe("POST");
|
|
62
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/cases`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
it("returns unauthorized envelope when AXIMO_API_KEY unset", async () => {
|
|
66
|
+
const prev = process.env["AXIMO_API_KEY"];
|
|
67
|
+
delete process.env["AXIMO_API_KEY"];
|
|
68
|
+
try {
|
|
69
|
+
const result = await runCaseCreate(MIN);
|
|
70
|
+
expect(result.ok).toBe(false);
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
expect(result.error.code).toBe("unauthorized");
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (prev !== undefined)
|
|
76
|
+
process.env["AXIMO_API_KEY"] = prev;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
it("maps 404 to not_found", async () => {
|
|
80
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, () => HttpResponse.json({ error: "Project not found" }, { status: 404 })));
|
|
81
|
+
const result = await runCaseCreate(MIN);
|
|
82
|
+
expect(result.ok).toBe(false);
|
|
83
|
+
if (!result.ok)
|
|
84
|
+
expect(result.error.code).toBe("not_found");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { CreateCaseResult, Envelope } from "../../types.js";
|
|
4
|
+
export interface CreateCaseOptions {
|
|
5
|
+
projectId?: unknown;
|
|
6
|
+
scenario?: unknown;
|
|
7
|
+
type?: unknown;
|
|
8
|
+
title?: unknown;
|
|
9
|
+
description?: unknown;
|
|
10
|
+
format?: unknown;
|
|
11
|
+
startUrl?: unknown;
|
|
12
|
+
directoryId?: unknown;
|
|
13
|
+
json?: unknown;
|
|
14
|
+
dryRun?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export declare function runCaseCreate(options: CreateCaseOptions, deps?: ClientDeps): Promise<Envelope<CreateCaseResult | DryRunResult>>;
|
|
17
|
+
declare const _default: import("citty").CommandDef<{
|
|
18
|
+
readonly "project-id": {
|
|
19
|
+
readonly type: "string";
|
|
20
|
+
};
|
|
21
|
+
readonly scenario: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
};
|
|
24
|
+
readonly type: {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
readonly description: "web | mobile | desktop";
|
|
27
|
+
};
|
|
28
|
+
readonly title: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
};
|
|
31
|
+
readonly description: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
};
|
|
34
|
+
readonly format: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
readonly description: "gherkin | markdown | plain_text";
|
|
37
|
+
};
|
|
38
|
+
readonly "start-url": {
|
|
39
|
+
readonly type: "string";
|
|
40
|
+
};
|
|
41
|
+
readonly "directory-id": {
|
|
42
|
+
readonly type: "string";
|
|
43
|
+
};
|
|
44
|
+
readonly json: {
|
|
45
|
+
readonly type: "string";
|
|
46
|
+
readonly description: "Raw JSON request body (replaces individual flags)";
|
|
47
|
+
};
|
|
48
|
+
readonly "dry-run": {
|
|
49
|
+
readonly type: "boolean";
|
|
50
|
+
readonly description: "Validate and show request without calling API";
|
|
51
|
+
};
|
|
52
|
+
}>;
|
|
53
|
+
export default _default;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { optionalString, requireString } from "../../validate.js";
|
|
7
|
+
export async function runCaseCreate(options, deps) {
|
|
8
|
+
let body;
|
|
9
|
+
const override = resolveJsonOverride(options.json);
|
|
10
|
+
if (override && "error" in override)
|
|
11
|
+
return override.error;
|
|
12
|
+
if (override) {
|
|
13
|
+
body = override.body;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
try {
|
|
17
|
+
body = {
|
|
18
|
+
projectId: requireString("projectId", options.projectId),
|
|
19
|
+
scenario: requireString("scenario", options.scenario),
|
|
20
|
+
type: optionalString("type", options.type),
|
|
21
|
+
title: optionalString("title", options.title),
|
|
22
|
+
description: optionalString("description", options.description),
|
|
23
|
+
format: optionalString("format", options.format),
|
|
24
|
+
startUrl: optionalString("startUrl", options.startUrl),
|
|
25
|
+
directoryId: optionalString("directoryId", options.directoryId),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return validationError(err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (options.dryRun) {
|
|
33
|
+
return dryRunEnvelope("POST", "/api/cases", body);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const res = await request("/api/cases", { method: "POST", body }, deps ?? {});
|
|
37
|
+
return formatSuccess(res);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return handleApiError(err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export default defineCommand({
|
|
44
|
+
meta: { name: "create", description: "Create a test case" },
|
|
45
|
+
args: {
|
|
46
|
+
"project-id": { type: "string" },
|
|
47
|
+
scenario: { type: "string" },
|
|
48
|
+
type: { type: "string", description: "web | mobile | desktop" },
|
|
49
|
+
title: { type: "string" },
|
|
50
|
+
description: { type: "string" },
|
|
51
|
+
format: { type: "string", description: "gherkin | markdown | plain_text" },
|
|
52
|
+
"start-url": { type: "string" },
|
|
53
|
+
"directory-id": { type: "string" },
|
|
54
|
+
json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
|
|
55
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
56
|
+
},
|
|
57
|
+
async run({ args }) {
|
|
58
|
+
printResult(await runCaseCreate({
|
|
59
|
+
projectId: args["project-id"],
|
|
60
|
+
scenario: args.scenario,
|
|
61
|
+
type: args.type,
|
|
62
|
+
title: args.title,
|
|
63
|
+
description: args.description,
|
|
64
|
+
format: args.format,
|
|
65
|
+
startUrl: args["start-url"],
|
|
66
|
+
directoryId: args["directory-id"],
|
|
67
|
+
json: args.json,
|
|
68
|
+
dryRun: args["dry-run"],
|
|
69
|
+
}));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
export default defineCommand({
|
|
3
|
-
meta: {
|
|
3
|
+
meta: {
|
|
4
|
+
name: "device",
|
|
5
|
+
description: "BYOD device commands (register/connect/status/...); 'device list' lists Device Farm mobile devices",
|
|
6
|
+
},
|
|
4
7
|
subCommands: {
|
|
5
8
|
list: () => import("./list.js").then((m) => m.default),
|
|
6
9
|
register: () => import("./register.js").then((m) => m.default),
|
|
@@ -26,7 +26,10 @@ export async function runDeviceList(options = {}, deps) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
export default defineCommand({
|
|
29
|
-
meta: {
|
|
29
|
+
meta: {
|
|
30
|
+
name: "list",
|
|
31
|
+
description: "List Device Farm mobile devices (ANDROID/IOS) — not BYOD; for your registered machines use 'device status'",
|
|
32
|
+
},
|
|
30
33
|
args: {
|
|
31
34
|
platform: { type: "string", description: "ANDROID | IOS" },
|
|
32
35
|
"available-only": { type: "boolean", description: "Only available devices" },
|
|
@@ -65,7 +65,7 @@ export async function runDeviceRegister(options, deps) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
export default defineCommand({
|
|
68
|
-
meta: { name: "register", description: "Register this machine as
|
|
68
|
+
meta: { name: "register", description: "Register this machine as a BYOD device" },
|
|
69
69
|
args: {
|
|
70
70
|
name: {
|
|
71
71
|
type: "string",
|
|
@@ -13,7 +13,10 @@ export async function runDeviceStatus(deps) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
export default defineCommand({
|
|
16
|
-
meta: {
|
|
16
|
+
meta: {
|
|
17
|
+
name: "status",
|
|
18
|
+
description: "List your registered BYOD devices with online/offline status — not Device Farm (see 'device list')",
|
|
19
|
+
},
|
|
17
20
|
async run() {
|
|
18
21
|
printResult(await runDeviceStatus());
|
|
19
22
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createMcpServer } from "./server.js";
|
|
4
|
+
import { getApiKey, getBaseUrl } from "../../client.js";
|
|
5
|
+
// --help / -h are intercepted in cli.ts before citty parses argv.
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "mcp",
|
|
9
|
+
description: "Run an MCP server exposing Aximo data over stdio",
|
|
10
|
+
},
|
|
11
|
+
args: {},
|
|
12
|
+
async run() {
|
|
13
|
+
// Fail fast before the JSON-RPC handshake; diagnostics go to stderr only.
|
|
14
|
+
try {
|
|
15
|
+
getApiKey();
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
process.stderr.write(`aximo mcp: ${message}\n`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const baseUrl = getBaseUrl();
|
|
24
|
+
process.stderr.write(`aximo mcp: serving stdio MCP against ${baseUrl}\n`);
|
|
25
|
+
const server = createMcpServer();
|
|
26
|
+
const transport = new StdioServerTransport();
|
|
27
|
+
await server.connect(transport);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { defineTools } from "./tools.js";
|
|
3
|
+
import pkg from "../../../package.json" with { type: "json" };
|
|
4
|
+
export function createMcpServer(deps = {}) {
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: "aximo",
|
|
7
|
+
version: pkg.version,
|
|
8
|
+
});
|
|
9
|
+
for (const tool of defineTools(deps)) {
|
|
10
|
+
server.registerTool(tool.name, {
|
|
11
|
+
title: tool.title,
|
|
12
|
+
description: tool.description,
|
|
13
|
+
inputSchema: tool.inputSchema,
|
|
14
|
+
},
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK's registerTool signature is generic over schema; safe at runtime
|
|
16
|
+
tool.handler);
|
|
17
|
+
}
|
|
18
|
+
return server;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|