@autifyhq/aximo-cli 0.3.0 → 0.5.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 +129 -35
- package/dist/cli.js +1 -0
- package/dist/commands/case/case.test.js +34 -0
- package/dist/commands/case/delete.d.ts +20 -0
- package/dist/commands/case/delete.js +39 -0
- package/dist/commands/case/index.js +1 -0
- package/dist/commands/device/device-config.d.ts +14 -0
- package/dist/commands/device/device-config.js +46 -0
- package/dist/commands/device/device-config.test.d.ts +1 -0
- package/dist/commands/device/device-config.test.js +56 -0
- package/dist/commands/device/doctor.d.ts +14 -0
- package/dist/commands/device/doctor.js +155 -29
- package/dist/commands/device/doctor.test.js +69 -3
- package/dist/commands/device/paths.d.ts +1 -0
- package/dist/commands/device/paths.js +3 -0
- package/dist/commands/device/register.d.ts +2 -0
- package/dist/commands/device/register.js +50 -32
- package/dist/commands/device/register.test.d.ts +1 -0
- package/dist/commands/device/register.test.js +95 -0
- package/dist/commands/device/unregister.d.ts +5 -3
- package/dist/commands/device/unregister.js +32 -33
- package/dist/commands/device/unregister.test.d.ts +1 -0
- package/dist/commands/device/unregister.test.js +73 -0
- package/dist/commands/mcp/server.test.js +2 -1
- package/dist/commands/mcp/tools.js +11 -0
- package/dist/commands/mcp/tools.test.js +12 -0
- package/dist/commands/mobile/connect-core.d.ts +12 -0
- package/dist/commands/mobile/connect-core.js +74 -0
- package/dist/commands/mobile/connect.d.ts +9 -0
- package/dist/commands/mobile/connect.js +35 -0
- package/dist/commands/mobile/connect.test.d.ts +1 -0
- package/dist/commands/mobile/connect.test.js +87 -0
- package/dist/commands/mobile/doctor.d.ts +28 -0
- package/dist/commands/mobile/doctor.js +143 -0
- package/dist/commands/mobile/index.d.ts +2 -0
- package/dist/commands/mobile/index.js +15 -0
- package/dist/commands/mobile/list.d.ts +10 -0
- package/dist/commands/mobile/list.js +30 -0
- package/dist/commands/mobile/paths.d.ts +14 -0
- package/dist/commands/mobile/paths.js +71 -0
- package/dist/commands/mobile/register-core.d.ts +19 -0
- package/dist/commands/mobile/register-core.js +46 -0
- package/dist/commands/mobile/register.d.ts +32 -0
- package/dist/commands/mobile/register.js +51 -0
- package/dist/commands/mobile/register.test.d.ts +1 -0
- package/dist/commands/mobile/register.test.js +60 -0
- package/dist/commands/mobile/setup.d.ts +27 -0
- package/dist/commands/mobile/setup.js +100 -0
- package/dist/commands/mobile/setup.test.d.ts +1 -0
- package/dist/commands/mobile/setup.test.js +49 -0
- package/dist/commands/mobile/unregister.d.ts +14 -0
- package/dist/commands/mobile/unregister.js +37 -0
- package/dist/commands/schema.js +22 -0
- package/dist/commands/session/create.d.ts +5 -0
- package/dist/commands/session/create.js +6 -0
- package/dist/desktop-server/setup-tls.test.d.ts +1 -0
- package/dist/desktop-server/setup-tls.test.js +83 -0
- package/dist/desktop-server/setup.d.ts +17 -0
- package/dist/desktop-server/setup.js +53 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mobile-control/appium-controller.d.ts +39 -0
- package/dist/mobile-control/appium-controller.js +367 -0
- package/dist/mobile-control/appium-controller.test.d.ts +1 -0
- package/dist/mobile-control/appium-controller.test.js +252 -0
- package/dist/mobile-control/appium-import.d.ts +7 -0
- package/dist/mobile-control/appium-import.js +6 -0
- package/dist/mobile-control/appium-import.test.d.ts +1 -0
- package/dist/mobile-control/appium-import.test.js +9 -0
- package/dist/mobile-control/appium-server.d.ts +30 -0
- package/dist/mobile-control/appium-server.js +110 -0
- package/dist/mobile-control/appium-server.test.d.ts +1 -0
- package/dist/mobile-control/appium-server.test.js +108 -0
- package/dist/mobile-control/controller.d.ts +22 -0
- package/dist/mobile-control/controller.js +1 -0
- package/dist/mobile-control/devices.d.ts +21 -0
- package/dist/mobile-control/devices.js +99 -0
- package/dist/mobile-control/devices.test.d.ts +1 -0
- package/dist/mobile-control/devices.test.js +35 -0
- package/dist/mobile-control/exec.d.ts +6 -0
- package/dist/mobile-control/exec.js +13 -0
- package/dist/mobile-control/tunnel.d.ts +11 -0
- package/dist/mobile-control/tunnel.js +152 -0
- package/dist/mobile-control/tunnel.test.d.ts +1 -0
- package/dist/mobile-control/tunnel.test.js +155 -0
- package/dist/mobile-control/webdriver.d.ts +31 -0
- package/dist/mobile-control/webdriver.js +137 -0
- package/dist/mobile-control/webdriver.test.d.ts +1 -0
- package/dist/mobile-control/webdriver.test.js +45 -0
- package/dist/types.d.ts +4 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -64,39 +64,46 @@ Credentials and device registrations are **per-environment** — to switch betwe
|
|
|
64
64
|
|
|
65
65
|
Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` returns machine-readable argument introspection (e.g. `aximo schema session.create`).
|
|
66
66
|
|
|
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
|
|
77
|
-
| `aximo plan
|
|
78
|
-
| `aximo plan
|
|
79
|
-
| `aximo
|
|
80
|
-
| `aximo trigger case
|
|
81
|
-
| `aximo trigger
|
|
82
|
-
| `aximo trigger plan
|
|
83
|
-
| `aximo
|
|
84
|
-
| `aximo upload
|
|
85
|
-
| `aximo upload
|
|
86
|
-
| `aximo upload
|
|
87
|
-
| `aximo upload
|
|
88
|
-
| `aximo
|
|
89
|
-
| `aximo device
|
|
90
|
-
| `aximo device
|
|
91
|
-
| `aximo device
|
|
92
|
-
| `aximo device
|
|
93
|
-
| `aximo device
|
|
94
|
-
| `aximo device
|
|
95
|
-
| `aximo device
|
|
96
|
-
| `aximo device
|
|
97
|
-
| `aximo device
|
|
98
|
-
| `aximo
|
|
99
|
-
| `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 case delete` | Delete a test case |
|
|
77
|
+
| `aximo plan create` | Create a test plan |
|
|
78
|
+
| `aximo plan add-case` | Add a case to a plan |
|
|
79
|
+
| `aximo plan remove-case` | Remove a case from a plan |
|
|
80
|
+
| `aximo trigger case run` | Trigger a dashboard-created test case |
|
|
81
|
+
| `aximo trigger case get` | Get triggered case status |
|
|
82
|
+
| `aximo trigger plan run` | Trigger a test plan |
|
|
83
|
+
| `aximo trigger plan get` | Get plan run status |
|
|
84
|
+
| `aximo upload create` | Create an upload slot for an APK/IPA |
|
|
85
|
+
| `aximo upload list` | List previously uploaded apps |
|
|
86
|
+
| `aximo upload get` | Poll upload processing status |
|
|
87
|
+
| `aximo upload put` | Upload binary file to the slot |
|
|
88
|
+
| `aximo upload submit` | Finalize upload and trigger processing |
|
|
89
|
+
| `aximo device list` | List mobile devices from Device Farm |
|
|
90
|
+
| `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
|
|
91
|
+
| `aximo device register` | Register this machine as a BYOD device |
|
|
92
|
+
| `aximo device connect` | Start the BYOD agent and connect to the relay |
|
|
93
|
+
| `aximo device status` | List your organization's BYOD devices |
|
|
94
|
+
| `aximo device ping` | Check your BYOD device's server-side status |
|
|
95
|
+
| `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
|
|
96
|
+
| `aximo device logs` | Show the BYOD agent log |
|
|
97
|
+
| `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
|
|
98
|
+
| `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
|
|
99
|
+
| `aximo mobile list` | List physically-connected phones (Android via adb) |
|
|
100
|
+
| `aximo mobile setup` | Install the Appium drivers used to drive phones |
|
|
101
|
+
| `aximo mobile doctor` | Diagnose Appium prerequisites (adb, SDK, driver) |
|
|
102
|
+
| `aximo mobile register` | Register a connected Android phone as an Aximo device |
|
|
103
|
+
| `aximo mobile connect` | Start managed Appium + open the tunnel for the phone |
|
|
104
|
+
| `aximo mobile unregister` | Remove a local mobile device server-side + local creds |
|
|
105
|
+
| `aximo schema` | Show CLI surface as JSON |
|
|
106
|
+
| `aximo mcp` | Run a stdio MCP server for agents (see below) |
|
|
100
107
|
|
|
101
108
|
Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
|
|
102
109
|
|
|
@@ -140,13 +147,15 @@ aximo case create --project-id <id> --scenario <text> \
|
|
|
140
147
|
[--format gherkin|markdown|plain_text] [--start-url <url>] \
|
|
141
148
|
[--directory-id <id>] [--json '<body>'] [--dry-run]
|
|
142
149
|
|
|
150
|
+
aximo case delete <caseId> [--dry-run]
|
|
151
|
+
|
|
143
152
|
aximo session save-as-case <sessionId> \
|
|
144
153
|
[--title <t>] [--type web|mobile|desktop] \
|
|
145
154
|
[--scenario <text>] [--format gherkin|markdown|plain_text] \
|
|
146
155
|
[--json '<body>'] [--dry-run]
|
|
147
156
|
```
|
|
148
157
|
|
|
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.
|
|
158
|
+
`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. `case delete` permanently removes a case: it is dropped from any plans it belongs to, and past run sessions are kept but detached.
|
|
150
159
|
|
|
151
160
|
### Test plans
|
|
152
161
|
|
|
@@ -247,6 +256,33 @@ aximo device connect # start the agent; keep the terminal open while tests r
|
|
|
247
256
|
|
|
248
257
|
Your device shows up in the Aximo UI under **Settings → Devices** with status `online` while connected. In the session form, pick your device as the execution target; Aximo routes the agent's actions through the tunnel to your machine.
|
|
249
258
|
|
|
259
|
+
### Self-hosted servers (self-signed / custom CA)
|
|
260
|
+
|
|
261
|
+
On-prem Aximo servers often present a self-signed certificate (a private "Aximo CA"). BYOD has **two** TLS clients: the Node CLI (API calls) and the embedded Python desktop agent (the `wss://…/devices/tunnel` relay). They trust a custom CA differently, so configure both:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# 1. Save the server's CA on this machine (or copy aximo-ca.pem from your admin).
|
|
265
|
+
openssl s_client -connect YOUR_SERVER:8443 -showcerts </dev/null 2>/dev/null \
|
|
266
|
+
| awk '/BEGIN CERTIFICATE/{i++} i==2{print} /END CERTIFICATE/{if(i==2) exit}' > ~/aximo-ca.pem
|
|
267
|
+
|
|
268
|
+
# 2. Point the CLI (Node) at it. Used for API calls AND auto-forwarded to the agent.
|
|
269
|
+
export NODE_EXTRA_CA_CERTS="$HOME/aximo-ca.pem"
|
|
270
|
+
|
|
271
|
+
aximo device connect
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
With `NODE_EXTRA_CA_CERTS` set, `device connect` automatically builds a combined trust store (the embedded Python's `certifi` roots **plus** your CA) and points the agent at it via `SSL_CERT_FILE`, so the relay connection verifies without extra steps.
|
|
275
|
+
|
|
276
|
+
To override the agent's trust store explicitly, set `SSL_CERT_FILE` yourself. It must include public roots **and** your CA — `SSL_CERT_FILE` replaces the entire store, so your CA alone would break any other TLS the agent does (browser launch, uploads):
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
PYBIN="$HOME/.aximo/python/bin/python3"
|
|
280
|
+
cat "$($PYBIN -m certifi)" ~/aximo-ca.pem > ~/.aximo/agent-ca-bundle.pem
|
|
281
|
+
export SSL_CERT_FILE="$HOME/.aximo/agent-ca-bundle.pem"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Persist whichever variables you use in your shell profile. Public-CA servers (e.g. `aximo.autify.com`) need none of this.
|
|
285
|
+
|
|
250
286
|
### Managing devices
|
|
251
287
|
|
|
252
288
|
```bash
|
|
@@ -358,6 +394,64 @@ Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Wi
|
|
|
358
394
|
|
|
359
395
|
> Devices are registered per-environment (see [Configuration](#configuration)) — a device registered against sandbox can't connect to production and vice versa.
|
|
360
396
|
|
|
397
|
+
## Mobile (local Android phone)
|
|
398
|
+
|
|
399
|
+
The `mobile` commands let you register a **physically-connected Android phone** as an Aximo device and drive it locally, over the same gateway tunnel as desktop BYOD. Aximo's agent controls the phone through a CLI-managed [Appium](https://appium.io) (UiAutomator2) server — useful for running mobile tests against your own handset instead of a Device Farm device.
|
|
400
|
+
|
|
401
|
+
> **Android only today.** iOS local devices are not yet supported — `aximo mobile register --platform ios` is blocked, and registration accepts only `--platform android`. (The Appium XCUITest plumbing is partially present but disabled; iOS local devices are planned for a future release.)
|
|
402
|
+
|
|
403
|
+
Terminology note: **`aximo mobile list`** shows phones plugged into _this_ machine; **`aximo device list`** (under [Device / BYOD](#device-byod--bring-your-own-device)) returns mobile devices from AWS Device Farm. Don't confuse the two.
|
|
404
|
+
|
|
405
|
+
### Prerequisites
|
|
406
|
+
|
|
407
|
+
- **Node.js 20+**, plus `aximo-cli` installed globally (see [Install](#install)).
|
|
408
|
+
- **An Aximo API key**, configured via `aximo configure` or env vars.
|
|
409
|
+
- **A USB-connected Android phone** with **USB debugging** enabled in Developer Options, plugged in, and authorized (accept the "Allow USB debugging?" prompt on the phone).
|
|
410
|
+
- **adb** (Android platform-tools) on your `PATH` — Appium delegates to it. Install via `brew install --cask android-platform-tools` (macOS), your distro's `adb` package (Linux), or the Android Studio SDK Manager (Windows).
|
|
411
|
+
- **The Android SDK**, with `ANDROID_HOME` (or `ANDROID_SDK_ROOT`) set — e.g. installed via Android Studio. Appium's UiAutomator2 driver requires it even though adb is already on PATH.
|
|
412
|
+
|
|
413
|
+
> The Appium server is bundled with the CLI, and the UiAutomator2 driver is installed by `aximo mobile setup` — you don't manage Appium yourself.
|
|
414
|
+
|
|
415
|
+
### First-time setup
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
aximo mobile setup # installs the UiAutomator2 Appium driver
|
|
419
|
+
aximo mobile doctor # checks adb / Android SDK / driver / bundled server
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`setup` installs the Appium driver, then prints what (if anything) is still missing. `doctor` is the first thing to run when anything's off — it reports the bundled Appium server, the UiAutomator2 driver, `adb`, and the Android SDK in one shot, and exits non-zero if a required check fails.
|
|
423
|
+
|
|
424
|
+
### Registering and connecting
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
aximo mobile list # see connected phones + their adb serials
|
|
428
|
+
aximo mobile register --name "My Pixel" --platform android # --platform defaults to android
|
|
429
|
+
aximo mobile connect # starts managed Appium + opens the tunnel
|
|
430
|
+
# Ctrl+C to disconnect
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`register` writes credentials to `$AXIMO_HOME/mobile-devices/<deviceId>.json` (mode `0600`). It targets the single connected phone by default — pass `--serial` to pick a specific one when several are attached. `connect` re-checks prerequisites, starts the managed Appium server, and opens the tunnel; it runs until Ctrl+C. If you have more than one registered phone, pass `--device-id` to choose which to connect.
|
|
434
|
+
|
|
435
|
+
Once connected, create a **mobile session in Aximo targeting this local device** — the agent routes its actions through the tunnel to the phone over Appium.
|
|
436
|
+
|
|
437
|
+
### Managing devices
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
aximo mobile unregister # remove the locally registered device (server-side + local creds)
|
|
441
|
+
aximo mobile unregister --device-id DEVICE_ID # target a specific device when several are registered
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Flags reference
|
|
445
|
+
|
|
446
|
+
| Command | Flags |
|
|
447
|
+
| ------------------------- | ----------------------------------------------------------------------------------- |
|
|
448
|
+
| `aximo mobile list` | _(none)_ |
|
|
449
|
+
| `aximo mobile setup` | _(none)_ |
|
|
450
|
+
| `aximo mobile doctor` | _(none)_ |
|
|
451
|
+
| `aximo mobile register` | `--name <name>` (required), `--platform android` (default), `--serial <adb-serial>` |
|
|
452
|
+
| `aximo mobile connect` | `--device-id <id>` (required only if multiple devices are registered) |
|
|
453
|
+
| `aximo mobile unregister` | `--device-id <id>` (defaults to the locally registered device) |
|
|
454
|
+
|
|
361
455
|
## MCP server
|
|
362
456
|
|
|
363
457
|
`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.
|
|
@@ -382,7 +476,7 @@ claude mcp add aximo -- aximo mcp
|
|
|
382
476
|
|
|
383
477
|
VS Code (Copilot agent mode) uses the same entry under a `servers` key in `.vscode/mcp.json`.
|
|
384
478
|
|
|
385
|
-
The server exposes
|
|
479
|
+
The server exposes 16 tools: 10 read-only ones for inspecting sessions and cases (`aximo_get_session_timeline` is the load-bearing one for Playwright export) plus 6 that author content (create and delete cases, create 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.
|
|
386
480
|
|
|
387
481
|
## Agent Skills
|
|
388
482
|
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ const main = defineCommand({
|
|
|
27
27
|
plan: () => import("./commands/plan/index.js").then((m) => m.default),
|
|
28
28
|
trigger: () => import("./commands/trigger/index.js").then((m) => m.default),
|
|
29
29
|
device: () => import("./commands/device/index.js").then((m) => m.default),
|
|
30
|
+
mobile: () => import("./commands/mobile/index.js").then((m) => m.default),
|
|
30
31
|
upload: () => import("./commands/upload/index.js").then((m) => m.default),
|
|
31
32
|
project: () => import("./commands/project/index.js").then((m) => m.default),
|
|
32
33
|
schema: () => import("./commands/schema.js").then((m) => m.default),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
2
|
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
3
|
import { runCaseCreate } from "./create.js";
|
|
4
|
+
import { runCaseDelete } from "./delete.js";
|
|
4
5
|
beforeAll(() => {
|
|
5
6
|
process.env["AXIMO_API_KEY"] = "test-key";
|
|
6
7
|
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
@@ -84,3 +85,36 @@ describe("runCaseCreate", () => {
|
|
|
84
85
|
expect(result.error.code).toBe("not_found");
|
|
85
86
|
});
|
|
86
87
|
});
|
|
88
|
+
describe("runCaseDelete", () => {
|
|
89
|
+
it("DELETEs the case path and returns success", async () => {
|
|
90
|
+
let called = false;
|
|
91
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/cases/c1`, () => {
|
|
92
|
+
called = true;
|
|
93
|
+
return HttpResponse.json({ success: true });
|
|
94
|
+
}));
|
|
95
|
+
const result = await runCaseDelete({ caseId: "c1" });
|
|
96
|
+
expect(result.ok).toBe(true);
|
|
97
|
+
expect(called).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it("rejects missing caseId", async () => {
|
|
100
|
+
const result = await runCaseDelete({});
|
|
101
|
+
expect(result.ok).toBe(false);
|
|
102
|
+
if (!result.ok)
|
|
103
|
+
expect(result.error.code).toBe("validation_error");
|
|
104
|
+
});
|
|
105
|
+
it("maps 404 to not_found", async () => {
|
|
106
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/cases/c1`, () => HttpResponse.json({ error: "Test case not found" }, { status: 404 })));
|
|
107
|
+
const result = await runCaseDelete({ caseId: "c1" });
|
|
108
|
+
expect(result.ok).toBe(false);
|
|
109
|
+
if (!result.ok)
|
|
110
|
+
expect(result.error.code).toBe("not_found");
|
|
111
|
+
});
|
|
112
|
+
it("--dry-run returns a DELETE request without calling API", async () => {
|
|
113
|
+
const result = await runCaseDelete({ caseId: "c1", dryRun: true });
|
|
114
|
+
expect(result.ok).toBe(true);
|
|
115
|
+
if (result.ok && "dryRun" in result.data) {
|
|
116
|
+
expect(result.data.method).toBe("DELETE");
|
|
117
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/cases/c1`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { DeleteCaseResult, Envelope } from "../../types.js";
|
|
4
|
+
export interface DeleteCaseOptions {
|
|
5
|
+
caseId?: unknown;
|
|
6
|
+
dryRun?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function runCaseDelete(options: DeleteCaseOptions, deps?: ClientDeps): Promise<Envelope<DeleteCaseResult | DryRunResult>>;
|
|
9
|
+
declare const _default: import("citty").CommandDef<{
|
|
10
|
+
readonly caseId: {
|
|
11
|
+
readonly type: "positional";
|
|
12
|
+
readonly description: "Case ID";
|
|
13
|
+
readonly required: true;
|
|
14
|
+
};
|
|
15
|
+
readonly "dry-run": {
|
|
16
|
+
readonly type: "boolean";
|
|
17
|
+
readonly description: "Validate and show request without calling API";
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { requireId } from "../../validate.js";
|
|
7
|
+
export async function runCaseDelete(options, deps) {
|
|
8
|
+
let caseId;
|
|
9
|
+
try {
|
|
10
|
+
caseId = requireId("caseId", options.caseId);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return validationError(err);
|
|
14
|
+
}
|
|
15
|
+
const path = `/api/cases/${encodeURIComponent(caseId)}`;
|
|
16
|
+
if (options.dryRun) {
|
|
17
|
+
return dryRunEnvelope("DELETE", path);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const res = await request(path, { method: "DELETE" }, deps ?? {});
|
|
21
|
+
return formatSuccess(res);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return handleApiError(err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export default defineCommand({
|
|
28
|
+
meta: { name: "delete", description: "Delete a test case" },
|
|
29
|
+
args: {
|
|
30
|
+
caseId: { type: "positional", description: "Case ID", required: true },
|
|
31
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
32
|
+
},
|
|
33
|
+
async run({ args }) {
|
|
34
|
+
printResult(await runCaseDelete({
|
|
35
|
+
caseId: args.caseId,
|
|
36
|
+
dryRun: args["dry-run"],
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DeviceConfig {
|
|
2
|
+
deviceId: string;
|
|
3
|
+
deviceToken: string;
|
|
4
|
+
name: string;
|
|
5
|
+
registeredAt: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Read and validate the local device registration (~/.aximo/device.json).
|
|
10
|
+
* Returns null when the file is missing, unreadable, or malformed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readDeviceConfig(): DeviceConfig | null;
|
|
13
|
+
/** Persist the local device registration with owner-only permissions. */
|
|
14
|
+
export declare function writeDeviceConfig(config: DeviceConfig): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { getAximoDir, getDeviceConfigPath } from "./paths.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read and validate the local device registration (~/.aximo/device.json).
|
|
5
|
+
* Returns null when the file is missing, unreadable, or malformed.
|
|
6
|
+
*/
|
|
7
|
+
export function readDeviceConfig() {
|
|
8
|
+
const path = getDeviceConfigPath();
|
|
9
|
+
if (!existsSync(path))
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
13
|
+
if (!parsed || typeof parsed !== "object")
|
|
14
|
+
return null;
|
|
15
|
+
const raw = parsed;
|
|
16
|
+
if (typeof raw.deviceId !== "string" ||
|
|
17
|
+
typeof raw.deviceToken !== "string" ||
|
|
18
|
+
typeof raw.name !== "string") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
deviceId: raw.deviceId,
|
|
23
|
+
deviceToken: raw.deviceToken,
|
|
24
|
+
name: raw.name,
|
|
25
|
+
registeredAt: typeof raw.registeredAt === "string" ? raw.registeredAt : "",
|
|
26
|
+
...(typeof raw.baseUrl === "string" ? { baseUrl: raw.baseUrl } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
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();
|
|
39
|
+
writeFileSync(path, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
40
|
+
try {
|
|
41
|
+
chmodSync(path, 0o600);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// chmod may fail on Windows; file-level ACLs apply instead.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { readDeviceConfig, writeDeviceConfig } from "./device-config.js";
|
|
6
|
+
let tempHome;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tempHome = mkdtempSync(join(tmpdir(), "aximo-device-config-"));
|
|
9
|
+
process.env["AXIMO_HOME"] = tempHome;
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
delete process.env["AXIMO_HOME"];
|
|
13
|
+
rmSync(tempHome, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
function devicePath() {
|
|
16
|
+
return join(tempHome, "device.json");
|
|
17
|
+
}
|
|
18
|
+
describe("device-config", () => {
|
|
19
|
+
it("returns null when the file is missing", () => {
|
|
20
|
+
expect(readDeviceConfig()).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it("round-trips a config including baseUrl", () => {
|
|
23
|
+
writeDeviceConfig({
|
|
24
|
+
deviceId: "d1",
|
|
25
|
+
deviceToken: "tok",
|
|
26
|
+
name: "mac",
|
|
27
|
+
registeredAt: "2026-06-29T00:00:00.000Z",
|
|
28
|
+
baseUrl: "https://srv:8443",
|
|
29
|
+
});
|
|
30
|
+
expect(readDeviceConfig()).toEqual({
|
|
31
|
+
deviceId: "d1",
|
|
32
|
+
deviceToken: "tok",
|
|
33
|
+
name: "mac",
|
|
34
|
+
registeredAt: "2026-06-29T00:00:00.000Z",
|
|
35
|
+
baseUrl: "https://srv:8443",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it("reads a pre-AT-577 file with no baseUrl (backward compatible)", () => {
|
|
39
|
+
writeFileSync(devicePath(), JSON.stringify({ deviceId: "d1", deviceToken: "tok", name: "mac", registeredAt: "t" }));
|
|
40
|
+
const cfg = readDeviceConfig();
|
|
41
|
+
expect(cfg).not.toBeNull();
|
|
42
|
+
expect(cfg?.deviceId).toBe("d1");
|
|
43
|
+
expect(cfg?.baseUrl).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
it("returns null for malformed json or a missing deviceId", () => {
|
|
46
|
+
writeFileSync(devicePath(), "{ not json");
|
|
47
|
+
expect(readDeviceConfig()).toBeNull();
|
|
48
|
+
writeFileSync(devicePath(), JSON.stringify({ deviceToken: "tok", name: "mac" }));
|
|
49
|
+
expect(readDeviceConfig()).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
it("writes the file with owner-only permissions", () => {
|
|
52
|
+
writeDeviceConfig({ deviceId: "d1", deviceToken: "tok", name: "mac", registeredAt: "t" });
|
|
53
|
+
// Sanity: file is valid JSON and readable back.
|
|
54
|
+
expect(JSON.parse(readFileSync(devicePath(), "utf-8"))).toMatchObject({ deviceId: "d1" });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -16,6 +16,20 @@ export interface DoctorReport {
|
|
|
16
16
|
info: number;
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
export declare function relayProbeUrl(): string;
|
|
20
|
+
export type TlsProbeResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
kind: "cert" | "conn";
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
export interface AgentTlsDeps {
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
probe?: (host: string, port: number, ca: Buffer) => Promise<TlsProbeResult>;
|
|
31
|
+
}
|
|
32
|
+
export declare function checkAgentTlsTrust(deps?: AgentTlsDeps): Promise<Check>;
|
|
19
33
|
export interface RunDoctorOptions {
|
|
20
34
|
skipRelay?: boolean;
|
|
21
35
|
skipApi?: boolean;
|