@autifyhq/aximo-cli 0.1.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/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +21 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +90 -0
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +127 -0
- package/dist/commands/configure.d.ts +24 -0
- package/dist/commands/configure.js +103 -0
- package/dist/commands/configure.test.d.ts +1 -0
- package/dist/commands/configure.test.js +96 -0
- package/dist/commands/device/connect.d.ts +2 -0
- package/dist/commands/device/connect.js +84 -0
- package/dist/commands/device/doctor.d.ts +34 -0
- package/dist/commands/device/doctor.js +421 -0
- package/dist/commands/device/doctor.test.d.ts +1 -0
- package/dist/commands/device/doctor.test.js +89 -0
- package/dist/commands/device/index.d.ts +2 -0
- package/dist/commands/device/index.js +16 -0
- package/dist/commands/device/list.d.ts +20 -0
- package/dist/commands/device/list.js +40 -0
- package/dist/commands/device/list.test.d.ts +1 -0
- package/dist/commands/device/list.test.js +38 -0
- package/dist/commands/device/logs.d.ts +34 -0
- package/dist/commands/device/logs.js +160 -0
- package/dist/commands/device/logs.test.d.ts +1 -0
- package/dist/commands/device/logs.test.js +70 -0
- package/dist/commands/device/paths.d.ts +12 -0
- package/dist/commands/device/paths.js +40 -0
- package/dist/commands/device/ping.d.ts +21 -0
- package/dist/commands/device/ping.js +66 -0
- package/dist/commands/device/ping.test.d.ts +1 -0
- package/dist/commands/device/ping.test.js +68 -0
- package/dist/commands/device/register.d.ts +23 -0
- package/dist/commands/device/register.js +83 -0
- package/dist/commands/device/reset.d.ts +30 -0
- package/dist/commands/device/reset.js +67 -0
- package/dist/commands/device/reset.test.d.ts +1 -0
- package/dist/commands/device/reset.test.js +69 -0
- package/dist/commands/device/setup.d.ts +25 -0
- package/dist/commands/device/setup.js +105 -0
- package/dist/commands/device/setup.test.d.ts +1 -0
- package/dist/commands/device/setup.test.js +64 -0
- package/dist/commands/device/status.d.ts +16 -0
- package/dist/commands/device/status.js +20 -0
- package/dist/commands/device/unregister.d.ts +14 -0
- package/dist/commands/device/unregister.js +60 -0
- package/dist/commands/health/check.d.ts +5 -0
- package/dist/commands/health/check.js +20 -0
- package/dist/commands/health/check.test.d.ts +1 -0
- package/dist/commands/health/check.test.js +26 -0
- package/dist/commands/health/index.d.ts +2 -0
- package/dist/commands/health/index.js +7 -0
- package/dist/commands/project/index.d.ts +2 -0
- package/dist/commands/project/index.js +7 -0
- package/dist/commands/project/list.d.ts +7 -0
- package/dist/commands/project/list.js +20 -0
- package/dist/commands/project/list.test.d.ts +1 -0
- package/dist/commands/project/list.test.js +28 -0
- package/dist/commands/schema.d.ts +27 -0
- package/dist/commands/schema.js +146 -0
- package/dist/commands/schema.test.d.ts +1 -0
- package/dist/commands/schema.test.js +38 -0
- package/dist/commands/session/create.d.ts +78 -0
- package/dist/commands/session/create.js +89 -0
- package/dist/commands/session/get.d.ts +11 -0
- package/dist/commands/session/get.js +30 -0
- package/dist/commands/session/index.d.ts +2 -0
- package/dist/commands/session/index.js +8 -0
- package/dist/commands/session/session.test.d.ts +1 -0
- package/dist/commands/session/session.test.js +128 -0
- package/dist/commands/trigger/case/get.d.ts +14 -0
- package/dist/commands/trigger/case/get.js +33 -0
- package/dist/commands/trigger/case/index.d.ts +2 -0
- package/dist/commands/trigger/case/index.js +8 -0
- package/dist/commands/trigger/case/run.d.ts +58 -0
- package/dist/commands/trigger/case/run.js +81 -0
- package/dist/commands/trigger/index.d.ts +2 -0
- package/dist/commands/trigger/index.js +8 -0
- package/dist/commands/trigger/plan/get.d.ts +14 -0
- package/dist/commands/trigger/plan/get.js +33 -0
- package/dist/commands/trigger/plan/index.d.ts +2 -0
- package/dist/commands/trigger/plan/index.js +8 -0
- package/dist/commands/trigger/plan/run.d.ts +33 -0
- package/dist/commands/trigger/plan/run.js +63 -0
- package/dist/commands/trigger/trigger.test.d.ts +1 -0
- package/dist/commands/trigger/trigger.test.js +107 -0
- package/dist/commands/upload/create.d.ts +32 -0
- package/dist/commands/upload/create.js +65 -0
- package/dist/commands/upload/get.d.ts +10 -0
- package/dist/commands/upload/get.js +30 -0
- package/dist/commands/upload/index.d.ts +2 -0
- package/dist/commands/upload/index.js +11 -0
- package/dist/commands/upload/list.d.ts +15 -0
- package/dist/commands/upload/list.js +31 -0
- package/dist/commands/upload/put.d.ts +24 -0
- package/dist/commands/upload/put.js +62 -0
- package/dist/commands/upload/submit.d.ts +19 -0
- package/dist/commands/upload/submit.js +36 -0
- package/dist/commands/upload/upload.test.d.ts +1 -0
- package/dist/commands/upload/upload.test.js +133 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/desktop-server/agent-sync.d.ts +19 -0
- package/dist/desktop-server/agent-sync.js +141 -0
- package/dist/desktop-server/agent-sync.test.d.ts +1 -0
- package/dist/desktop-server/agent-sync.test.js +130 -0
- package/dist/desktop-server/setup.d.ts +19 -0
- package/dist/desktop-server/setup.js +200 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +71 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +88 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/mutate.d.ts +13 -0
- package/dist/mutate.js +25 -0
- package/dist/mutate.test.d.ts +1 -0
- package/dist/mutate.test.js +49 -0
- package/dist/output.d.ts +5 -0
- package/dist/output.js +23 -0
- package/dist/output.test.d.ts +1 -0
- package/dist/output.test.js +42 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/test-utils.d.ts +4 -0
- package/dist/test-utils.js +5 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.js +2 -0
- package/dist/validate.d.ts +14 -0
- package/dist/validate.js +103 -0
- package/dist/validate.test.d.ts +1 -0
- package/dist/validate.test.js +96 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Autify Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# @autifyhq/aximo-cli
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@autifyhq/aximo-cli)
|
|
4
|
+
[](https://github.com/autifyhq/aximo-cli/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
CLI for the [Autify Aximo API](https://aximo.autify.com/openapi.yaml), built for coding agents. Every command returns a JSON envelope (`{ok, data}` or `{ok: false, error}`) so output pipes cleanly into scripts and other tools.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -g @autifyhq/aximo-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 1. Configure credentials (interactive) — or set env vars, see Configuration below
|
|
19
|
+
aximo configure
|
|
20
|
+
|
|
21
|
+
# 2. Verify the connection
|
|
22
|
+
aximo health check
|
|
23
|
+
|
|
24
|
+
# 3. Run a desktop test session
|
|
25
|
+
aximo project list
|
|
26
|
+
aximo session create \
|
|
27
|
+
--project-id PROJECT_ID \
|
|
28
|
+
--title "Login test" \
|
|
29
|
+
--prompt "Go to https://example.com and verify the page loads."
|
|
30
|
+
|
|
31
|
+
# 4. Poll session status
|
|
32
|
+
aximo session get SESSION_ID
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Get your API key from **Settings → API Keys** in the [Aximo dashboard](https://aximo.autify.com).
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
The CLI resolves each credential independently in this order:
|
|
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`)
|
|
44
|
+
|
|
45
|
+
| Variable | Required | Default | Notes |
|
|
46
|
+
| ---------------- | -------- | -------------------------- | ------------------------------------------------------------ |
|
|
47
|
+
| `AXIMO_API_KEY` | Yes | — | Organization API key |
|
|
48
|
+
| `AXIMO_BASE_URL` | No | `https://aximo.autify.com` | Override for sandbox / self-hosted |
|
|
49
|
+
| `AXIMO_HOME` | No | `~/.aximo` | Data directory (config, device credentials, embedded Python) |
|
|
50
|
+
|
|
51
|
+
### Using `aximo configure`
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
aximo configure # interactive prompts
|
|
55
|
+
aximo configure --base-url URL --api-key KEY # non-interactive
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The interactive flow prompts first for the base URL (empty → keep existing, else falls back to the Aximo default), then for the API key (empty → keep existing; errors if nothing is saved). Config is written to `$AXIMO_HOME/config.json` with mode `0600`. To change sandbox/prod, re-run `aximo configure` and enter a new URL.
|
|
59
|
+
|
|
60
|
+
### Using environment variables
|
|
61
|
+
|
|
62
|
+
If you'd rather keep credentials out of a file (CI, ephemeral shells), set env vars:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# macOS / Linux
|
|
66
|
+
export AXIMO_API_KEY=your-api-key
|
|
67
|
+
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
|
+
```
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` returns machine-readable argument introspection (e.g. `aximo schema session.create`).
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
| ------------------------- | ---------------------------------------------------- |
|
|
80
|
+
| `aximo configure` | Save credentials to `$AXIMO_HOME/config.json` |
|
|
81
|
+
| `aximo health check` | Check API reachability |
|
|
82
|
+
| `aximo project list` | List organization projects |
|
|
83
|
+
| `aximo session create` | Create and queue a test session |
|
|
84
|
+
| `aximo session get` | Get session status |
|
|
85
|
+
| `aximo trigger case run` | Trigger a dashboard-created test case |
|
|
86
|
+
| `aximo trigger case get` | Get triggered case status |
|
|
87
|
+
| `aximo trigger plan run` | Trigger a test plan |
|
|
88
|
+
| `aximo trigger plan get` | Get plan run status |
|
|
89
|
+
| `aximo upload create` | Create an upload slot for an APK/IPA |
|
|
90
|
+
| `aximo upload list` | List previously uploaded apps |
|
|
91
|
+
| `aximo upload get` | Poll upload processing status |
|
|
92
|
+
| `aximo upload put` | Upload binary file to the slot |
|
|
93
|
+
| `aximo upload submit` | Finalize upload and trigger processing |
|
|
94
|
+
| `aximo device list` | List mobile devices from Device Farm |
|
|
95
|
+
| `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
|
|
96
|
+
| `aximo device register` | Register this machine as a BYOD device |
|
|
97
|
+
| `aximo device connect` | Start the BYOD agent and connect to the relay |
|
|
98
|
+
| `aximo device status` | List your organization's BYOD devices |
|
|
99
|
+
| `aximo device ping` | Check your BYOD device's server-side status |
|
|
100
|
+
| `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
|
|
101
|
+
| `aximo device logs` | Show the BYOD agent log |
|
|
102
|
+
| `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
|
|
103
|
+
| `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
|
|
104
|
+
| `aximo schema` | Show CLI surface as JSON |
|
|
105
|
+
|
|
106
|
+
Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
|
|
107
|
+
|
|
108
|
+
### Health
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
aximo health check
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Returns `{ok: true, data: {status: "ok"}}` when the API is reachable and your API key is valid.
|
|
115
|
+
|
|
116
|
+
### Projects
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
aximo project list
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Lists active workspaces in your organization. Grab the `id` to pass as `--project-id` for session creation.
|
|
123
|
+
|
|
124
|
+
### Sessions
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
aximo session create --title <t> --prompt <p> --project-id <id> \
|
|
128
|
+
[--platform ANDROID|IOS] [--model <m>] [--start-url <url>] \
|
|
129
|
+
[--case-id <id>] [--tags <comma-separated>] \
|
|
130
|
+
[--device-arn <arn>] [--device-name <name>] \
|
|
131
|
+
[--upload-arn <arn>] [--upload-type ANDROID_APP|IOS_APP] \
|
|
132
|
+
[--app-name <name>] [--bundle-id <id>] \
|
|
133
|
+
[--json '<body>'] [--dry-run]
|
|
134
|
+
|
|
135
|
+
aximo session get <sessionId>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Desktop sessions default to `platform=desktop`, `model=sonnet`. Mobile sessions require both `--device-arn` and `--upload-arn`.
|
|
139
|
+
|
|
140
|
+
### Triggers (test cases and plans)
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
aximo trigger case run <caseId> [...]
|
|
144
|
+
aximo trigger case get <caseId> <sessionId>
|
|
145
|
+
aximo trigger plan run <planId> [...]
|
|
146
|
+
aximo trigger plan get <planId> <planRunId>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Triggers dashboard-created test cases or plans. API access must be enabled in the Aximo dashboard settings. `trigger case run` returns a `sessionId` you can poll; `trigger plan run` returns per-case breakdown.
|
|
150
|
+
|
|
151
|
+
### Uploads
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
aximo upload create --file-name <name> [--file-size <bytes>] [--platform ANDROID|IOS]
|
|
155
|
+
aximo upload put <uploadId> --file <path>
|
|
156
|
+
aximo upload submit <uploadId>
|
|
157
|
+
aximo upload get <uploadId>
|
|
158
|
+
aximo upload list [--file-type ANDROID_APP|IOS_APP]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Typical flow: `create` → `put` (sends binary to the presigned URL) → `submit` → poll `get` until `status=succeeded` → extract `uploadArn` for mobile sessions. `upload list` auto-re-uploads expired Device Farm entries.
|
|
162
|
+
|
|
163
|
+
### Schema
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
aximo schema # all commands
|
|
167
|
+
aximo schema session.create # one command's arg schema
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Machine-readable JSON view of the CLI surface — used by agent skills for discovery.
|
|
171
|
+
|
|
172
|
+
## Device (BYOD — Bring Your Own Device)
|
|
173
|
+
|
|
174
|
+
The `device` commands let you register your own desktop as a test execution target. Aximo's agent controls it remotely over WebSocket to run tests against apps on your machine (useful when tests need access to your LAN, VPN, or a specific desktop environment).
|
|
175
|
+
|
|
176
|
+
Terminology note: **`aximo device list` returns mobile devices** from Device Farm; **`aximo device status` returns BYOD devices** you've registered. Don't confuse the two.
|
|
177
|
+
|
|
178
|
+
### Prerequisites
|
|
179
|
+
|
|
180
|
+
- **Node.js 20+**, plus `aximo-cli` installed globally (see [Install](#install)).
|
|
181
|
+
- **An Aximo API key**, configured via `aximo configure` or env vars.
|
|
182
|
+
- **Active desktop session** — the agent uses `pyautogui` for screenshots and input, so the screen must be unlocked and awake while tests run.
|
|
183
|
+
- **Outbound WebSocket to port 443** — most corporate firewalls allow this; if yours doesn't, see [Troubleshooting](#byod-troubleshooting).
|
|
184
|
+
|
|
185
|
+
> Python is **not** a prerequisite. On first `connect` (or via `device setup`), the CLI downloads a self-contained CPython build into `$AXIMO_HOME/python/` (~35 MB download, ~130 MB installed) and installs the required packages. Your system Python is untouched.
|
|
186
|
+
|
|
187
|
+
### First-time setup
|
|
188
|
+
|
|
189
|
+
One-command onboarding:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
aximo device setup --name "My Windows PC"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This verifies API reachability, installs the embedded Python runtime, installs Python dependencies, and registers this machine. If the device is already registered locally, it skips registration and reports `alreadyRegistered: true`.
|
|
196
|
+
|
|
197
|
+
Or step-by-step, if you'd rather see each stage:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
aximo health check # confirm credentials + connectivity
|
|
201
|
+
aximo device register --name "My Windows PC" # creates server-side device, writes $AXIMO_HOME/device.json (0600)
|
|
202
|
+
aximo device connect # installs embedded Python on first run, then connects
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
On success, `device connect` prints something like:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Preparing Python environment...
|
|
209
|
+
Fetching embedded Python 3.12.13 (x86_64-pc-windows-msvc)...
|
|
210
|
+
Extracting...
|
|
211
|
+
Installing Python dependencies...
|
|
212
|
+
Python ready: C:\Users\<you>\.aximo\python\python.exe
|
|
213
|
+
Agent version: 8f3a1c04
|
|
214
|
+
Starting desktop agent, relay: wss://aximo.autify.com/devices/tunnel
|
|
215
|
+
Device "My Windows PC" is online. Press Ctrl+C to disconnect.
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Subsequent `connect` runs skip the Python download and dep install — they re-run only when the server-hosted agent script changes.
|
|
219
|
+
|
|
220
|
+
### Daily use
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
aximo device connect # start the agent; keep the terminal open while tests run
|
|
224
|
+
# Ctrl+C to disconnect
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
### Managing devices
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
aximo device status # list YOUR BYOD devices, with online/offline + lastSeenAt
|
|
233
|
+
aximo device ping # server-side status for the locally registered device
|
|
234
|
+
aximo device ping --device-id DEVICE_ID # same, for a specific device
|
|
235
|
+
aximo device unregister # delete server-side + clear local credentials
|
|
236
|
+
aximo device unregister --device-id DEVICE_ID # unregister an orphaned device (no local creds)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`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."
|
|
240
|
+
|
|
241
|
+
### Re-registering
|
|
242
|
+
|
|
243
|
+
`aximo device register` refuses to overwrite an existing local registration. To move a registration to a different device name:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
aximo device unregister # clean up the old one server-side
|
|
247
|
+
aximo device register --name "New Name"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If you've lost the original local credentials and need to delete the orphaned server-side device:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
aximo device status # find its deviceId
|
|
254
|
+
aximo device unregister --device-id DEVICE_ID
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Or use `aximo device register --force` — but that silently orphans the previous server-side device, so the explicit `unregister` + `register` path is safer.
|
|
258
|
+
|
|
259
|
+
### Inspecting and resetting local state
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
aximo device logs # tail the last 50 lines of the agent log
|
|
263
|
+
aximo device logs --follow # stream new lines (not JSON-enveloped; human use)
|
|
264
|
+
aximo device logs --path # print the log file path and exit
|
|
265
|
+
aximo device logs --clear # truncate the log
|
|
266
|
+
|
|
267
|
+
aximo device doctor # run all checks: env, API, Python, relay, status
|
|
268
|
+
aximo device doctor --skip-relay # skip WebSocket reachability probe
|
|
269
|
+
aximo device doctor --skip-api # offline checks only
|
|
270
|
+
|
|
271
|
+
aximo device reset --python # remove embedded Python + its cache
|
|
272
|
+
aximo device reset --deps # reinstall deps on next setup
|
|
273
|
+
aximo device reset --device # delete local credentials (server-side stays; run `unregister` first to also delete there)
|
|
274
|
+
aximo device reset --all # wipe $AXIMO_HOME entirely
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`doctor` is the first thing to run when anything's off — it covers the whole stack in one shot.
|
|
278
|
+
|
|
279
|
+
### BYOD troubleshooting
|
|
280
|
+
|
|
281
|
+
**"No API key configured."**
|
|
282
|
+
Either run `aximo configure`, or set `AXIMO_API_KEY` in your environment. Verify with `echo $AXIMO_API_KEY` (or `echo $env:AXIMO_API_KEY` on PowerShell).
|
|
283
|
+
|
|
284
|
+
**"Authentication failed."**
|
|
285
|
+
Your device token is invalid (deleted server-side, or the server URL changed). Clean up and re-register:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
aximo device unregister
|
|
289
|
+
aximo device register --name "My PC"
|
|
290
|
+
aximo device connect
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**"Disconnected. Reconnecting in Xs..."**
|
|
294
|
+
Normal for brief network blips — the agent retries with exponential backoff (1 s → 30 s max). If it keeps failing:
|
|
295
|
+
|
|
296
|
+
- Check your internet connection.
|
|
297
|
+
- Confirm `AXIMO_BASE_URL` is correct and reachable: `aximo health check`.
|
|
298
|
+
- Confirm outbound WebSocket on 443 isn't blocked: `aximo device doctor` runs a handshake probe.
|
|
299
|
+
|
|
300
|
+
**Python download or pip install fails (corporate proxy).**
|
|
301
|
+
The embedded Python is fetched from `github.com/astral-sh/python-build-standalone/releases`; packages come from PyPI. Behind a proxy:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# macOS / Linux
|
|
305
|
+
~/.aximo/python/bin/python3 -m pip config set global.proxy http://your-proxy:port
|
|
306
|
+
|
|
307
|
+
# Windows
|
|
308
|
+
%USERPROFILE%\.aximo\python\python.exe -m pip config set global.proxy http://your-proxy:port
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
To retry from a clean slate: `aximo device reset --python && aximo device setup`.
|
|
312
|
+
|
|
313
|
+
**Device shows online, but tests fail on screenshot / click.**
|
|
314
|
+
Your screen is locked or the session is inactive. `pyautogui` needs an active desktop session.
|
|
315
|
+
|
|
316
|
+
**macOS: actions seem to do nothing.**
|
|
317
|
+
System Settings → Privacy & Security → Accessibility and Screen Recording — both need to be granted to the terminal (or the Python binary that spawned).
|
|
318
|
+
|
|
319
|
+
**Linux: "missing: scrot, xclip".**
|
|
320
|
+
`aximo device doctor` flags this when the tools aren't on PATH. Install via your package manager (`apt install scrot xclip` on Debian/Ubuntu).
|
|
321
|
+
|
|
322
|
+
**Windows: coordinates off / scrolling too fast or too slow.**
|
|
323
|
+
The agent disables DPI-awareness and uses a calibrated scroll amount — if you still see mismatches, run `aximo device doctor` and share the output.
|
|
324
|
+
|
|
325
|
+
### Environment & file layout
|
|
326
|
+
|
|
327
|
+
| Path | Mode | Purpose |
|
|
328
|
+
| --------------------------------- | ---- | -------------------------------------------------------- |
|
|
329
|
+
| `$AXIMO_HOME/config.json` | 0600 | CLI credentials (from `aximo configure`) |
|
|
330
|
+
| `$AXIMO_HOME/device.json` | 0600 | BYOD registration + device token |
|
|
331
|
+
| `$AXIMO_HOME/agent/remote.py` | 0644 | Python agent (server-synced) |
|
|
332
|
+
| `$AXIMO_HOME/agent/manifest.json` | 0644 | Version + sha256 of cached agent files |
|
|
333
|
+
| `$AXIMO_HOME/python/` | — | Embedded CPython runtime |
|
|
334
|
+
| `$AXIMO_HOME/remote.log` | — | Agent log (rotate/clear via `aximo device logs --clear`) |
|
|
335
|
+
|
|
336
|
+
Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Windows. Override via the `AXIMO_HOME` env var.
|
|
337
|
+
|
|
338
|
+
### Sandbox vs production
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Sandbox
|
|
342
|
+
aximo configure --base-url https://aximo-sbx.autify.dev --api-key <sbx-key>
|
|
343
|
+
|
|
344
|
+
# Production (default)
|
|
345
|
+
aximo configure --base-url https://aximo.autify.com --api-key <prod-key>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Registrations are per-environment — a device registered against sandbox can't connect to production and vice versa.
|
|
349
|
+
|
|
350
|
+
## Agent Skills
|
|
351
|
+
|
|
352
|
+
Ships with an [agent skill](https://skills.sh/). Once installed, the agent discovers commands via `aximo schema` and follows workflows in `skills/aximo-cli/`.
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
npx skills add autifyhq/aximo-cli
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
See [commands reference](./skills/aximo-cli/references/commands.md) and [workflows](./skills/aximo-cli/references/workflows.md) for the agent-oriented docs.
|
|
359
|
+
|
|
360
|
+
## Contributing
|
|
361
|
+
|
|
362
|
+
### Requirements
|
|
363
|
+
|
|
364
|
+
- [mise](https://mise.jdx.dev/)
|
|
365
|
+
|
|
366
|
+
### Preparation
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
pnpm install
|
|
370
|
+
pnpm run cli -- health check
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Release Process
|
|
374
|
+
|
|
375
|
+
Automated via [release-please](https://github.com/googleapis/release-please):
|
|
376
|
+
|
|
377
|
+
1. Merge commits to `main` with Conventional Commit messages
|
|
378
|
+
2. release-please creates/updates a Release PR with CHANGELOG.md
|
|
379
|
+
3. Merge the Release PR to publish a GitHub Release and npm package
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import pkg from "../package.json" with { type: "json" };
|
|
4
|
+
const main = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "aximo",
|
|
7
|
+
version: pkg.version,
|
|
8
|
+
description: "Autify Aximo CLI for coding agents",
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
configure: () => import("./commands/configure.js").then((m) => m.default),
|
|
12
|
+
health: () => import("./commands/health/index.js").then((m) => m.default),
|
|
13
|
+
session: () => import("./commands/session/index.js").then((m) => m.default),
|
|
14
|
+
trigger: () => import("./commands/trigger/index.js").then((m) => m.default),
|
|
15
|
+
device: () => import("./commands/device/index.js").then((m) => m.default),
|
|
16
|
+
upload: () => import("./commands/upload/index.js").then((m) => m.default),
|
|
17
|
+
project: () => import("./commands/project/index.js").then((m) => m.default),
|
|
18
|
+
schema: () => import("./commands/schema.js").then((m) => m.default),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
runMain(main);
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const DEFAULT_BASE_URL = "https://aximo.autify.com";
|
|
2
|
+
export declare function getApiKey(): string;
|
|
3
|
+
export declare function getBaseUrl(): string;
|
|
4
|
+
export interface RequestOptions {
|
|
5
|
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
6
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
rawBody?: BodyInit | undefined;
|
|
9
|
+
contentType?: string | undefined;
|
|
10
|
+
expectJson?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ClientDeps {
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
export declare function request<T>(path: string, options?: RequestOptions, deps?: ClientDeps): Promise<T>;
|
|
18
|
+
export declare function requestBuffer(path: string, deps?: ClientDeps): Promise<Buffer>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
export const DEFAULT_BASE_URL = "https://aximo.autify.com";
|
|
3
|
+
export function getApiKey() {
|
|
4
|
+
const config = loadConfig();
|
|
5
|
+
const key = config.apiKey ?? process.env["AXIMO_API_KEY"];
|
|
6
|
+
if (!key) {
|
|
7
|
+
throw new Error("No API key configured. Run `aximo configure`, set AXIMO_API_KEY in your environment, or use `node --env-file=.env`.");
|
|
8
|
+
}
|
|
9
|
+
return key;
|
|
10
|
+
}
|
|
11
|
+
export function getBaseUrl() {
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
const raw = config.baseUrl ?? process.env["AXIMO_BASE_URL"] ?? DEFAULT_BASE_URL;
|
|
14
|
+
return raw.replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
function httpError(status, message, retryAfter) {
|
|
17
|
+
const err = Object.assign(new Error(message), { status });
|
|
18
|
+
if (retryAfter !== undefined)
|
|
19
|
+
err.retryAfter = retryAfter;
|
|
20
|
+
return err;
|
|
21
|
+
}
|
|
22
|
+
async function errorMessage(res) {
|
|
23
|
+
const text = await res.text().catch(() => "");
|
|
24
|
+
if (!text)
|
|
25
|
+
return res.statusText || `HTTP ${res.status}`;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(text);
|
|
28
|
+
if (typeof parsed.error === "string")
|
|
29
|
+
return parsed.error;
|
|
30
|
+
if (typeof parsed.message === "string")
|
|
31
|
+
return parsed.message;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// fall through
|
|
35
|
+
}
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
function buildUrl(baseUrl, path, query) {
|
|
39
|
+
const url = new URL(path.startsWith("/") ? path : `/${path}`, `${baseUrl}/`);
|
|
40
|
+
if (query) {
|
|
41
|
+
for (const [k, v] of Object.entries(query)) {
|
|
42
|
+
if (v === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
url.searchParams.set(k, String(v));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return url.toString();
|
|
48
|
+
}
|
|
49
|
+
export async function request(path, options = {}, deps = {}) {
|
|
50
|
+
const apiKey = deps.apiKey ?? getApiKey();
|
|
51
|
+
const baseUrl = deps.baseUrl ?? getBaseUrl();
|
|
52
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
53
|
+
const method = options.method ?? "GET";
|
|
54
|
+
const url = buildUrl(baseUrl, path, options.query);
|
|
55
|
+
const headers = { "x-api-key": apiKey };
|
|
56
|
+
let body;
|
|
57
|
+
if (options.rawBody !== undefined) {
|
|
58
|
+
body = options.rawBody;
|
|
59
|
+
if (options.contentType)
|
|
60
|
+
headers["Content-Type"] = options.contentType;
|
|
61
|
+
}
|
|
62
|
+
else if (options.body !== undefined) {
|
|
63
|
+
headers["Content-Type"] = "application/json";
|
|
64
|
+
body = JSON.stringify(options.body);
|
|
65
|
+
}
|
|
66
|
+
const res = await fetchImpl(url, { method, headers, ...(body !== undefined ? { body } : {}) });
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const retryAfter = res.headers.get("retry-after") ?? undefined;
|
|
69
|
+
throw httpError(res.status, await errorMessage(res), retryAfter);
|
|
70
|
+
}
|
|
71
|
+
if (options.expectJson === false) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const text = await res.text();
|
|
75
|
+
if (!text)
|
|
76
|
+
return undefined;
|
|
77
|
+
return JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
export async function requestBuffer(path, deps = {}) {
|
|
80
|
+
const apiKey = deps.apiKey ?? getApiKey();
|
|
81
|
+
const baseUrl = deps.baseUrl ?? getBaseUrl();
|
|
82
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
83
|
+
const url = buildUrl(baseUrl, path);
|
|
84
|
+
const res = await fetchImpl(url, { method: "GET", headers: { "x-api-key": apiKey } });
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const retryAfter = res.headers.get("retry-after") ?? undefined;
|
|
87
|
+
throw httpError(res.status, await errorMessage(res), retryAfter);
|
|
88
|
+
}
|
|
89
|
+
return Buffer.from(await res.arrayBuffer());
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { server, http, HttpResponse, TEST_BASE_URL } from "./test-utils.js";
|
|
3
|
+
import { getApiKey, getBaseUrl, request } from "./client.js";
|
|
4
|
+
const ORIGINAL_KEY = process.env["AXIMO_API_KEY"];
|
|
5
|
+
const ORIGINAL_URL = process.env["AXIMO_BASE_URL"];
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
process.env["AXIMO_API_KEY"] = "test-key";
|
|
8
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
9
|
+
server.listen();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => server.resetHandlers());
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
server.close();
|
|
14
|
+
if (ORIGINAL_KEY !== undefined)
|
|
15
|
+
process.env["AXIMO_API_KEY"] = ORIGINAL_KEY;
|
|
16
|
+
else
|
|
17
|
+
delete process.env["AXIMO_API_KEY"];
|
|
18
|
+
if (ORIGINAL_URL !== undefined)
|
|
19
|
+
process.env["AXIMO_BASE_URL"] = ORIGINAL_URL;
|
|
20
|
+
else
|
|
21
|
+
delete process.env["AXIMO_BASE_URL"];
|
|
22
|
+
});
|
|
23
|
+
describe("getApiKey", () => {
|
|
24
|
+
it("returns AXIMO_API_KEY when set", () => {
|
|
25
|
+
expect(getApiKey()).toBe("test-key");
|
|
26
|
+
});
|
|
27
|
+
it("throws when unset", () => {
|
|
28
|
+
const prev = process.env["AXIMO_API_KEY"];
|
|
29
|
+
delete process.env["AXIMO_API_KEY"];
|
|
30
|
+
try {
|
|
31
|
+
expect(() => getApiKey()).toThrow("AXIMO_API_KEY");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
if (prev !== undefined)
|
|
35
|
+
process.env["AXIMO_API_KEY"] = prev;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("getBaseUrl", () => {
|
|
40
|
+
it("returns override without trailing slash", () => {
|
|
41
|
+
process.env["AXIMO_BASE_URL"] = "https://example.test/";
|
|
42
|
+
expect(getBaseUrl()).toBe("https://example.test");
|
|
43
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
44
|
+
});
|
|
45
|
+
it("defaults to https://aximo.autify.com", () => {
|
|
46
|
+
delete process.env["AXIMO_BASE_URL"];
|
|
47
|
+
expect(getBaseUrl()).toBe("https://aximo.autify.com");
|
|
48
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("request", () => {
|
|
52
|
+
it("sends x-api-key header and parses JSON", async () => {
|
|
53
|
+
let seenKey = null;
|
|
54
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, ({ request: req }) => {
|
|
55
|
+
seenKey = req.headers.get("x-api-key");
|
|
56
|
+
return HttpResponse.json({ ok: "yes" });
|
|
57
|
+
}));
|
|
58
|
+
const result = await request("/api/health");
|
|
59
|
+
expect(result).toEqual({ ok: "yes" });
|
|
60
|
+
expect(seenKey).toBe("test-key");
|
|
61
|
+
});
|
|
62
|
+
it("encodes query string", async () => {
|
|
63
|
+
let seenUrl = "";
|
|
64
|
+
server.use(http.get(`${TEST_BASE_URL}/api/devices`, ({ request: req }) => {
|
|
65
|
+
seenUrl = req.url;
|
|
66
|
+
return HttpResponse.json({ devices: [] });
|
|
67
|
+
}));
|
|
68
|
+
await request("/api/devices", { query: { platform: "IOS", availableOnly: true } });
|
|
69
|
+
expect(seenUrl).toContain("platform=IOS");
|
|
70
|
+
expect(seenUrl).toContain("availableOnly=true");
|
|
71
|
+
});
|
|
72
|
+
it("POSTs JSON body with content-type", async () => {
|
|
73
|
+
let seenBody;
|
|
74
|
+
let seenCT = "";
|
|
75
|
+
server.use(http.post(`${TEST_BASE_URL}/api/sessions`, async ({ request: req }) => {
|
|
76
|
+
seenCT = req.headers.get("content-type") ?? "";
|
|
77
|
+
seenBody = await req.json();
|
|
78
|
+
return HttpResponse.json({ success: true, sessionId: "s1" }, { status: 201 });
|
|
79
|
+
}));
|
|
80
|
+
const result = await request("/api/sessions", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: { title: "t" },
|
|
83
|
+
});
|
|
84
|
+
expect(result.sessionId).toBe("s1");
|
|
85
|
+
expect(seenCT).toContain("application/json");
|
|
86
|
+
expect(seenBody).toEqual({ title: "t" });
|
|
87
|
+
});
|
|
88
|
+
it("throws HttpError with status on non-2xx", async () => {
|
|
89
|
+
server.use(http.get(`${TEST_BASE_URL}/api/boom`, () => HttpResponse.json({ error: "Unauthorized" }, { status: 401 })));
|
|
90
|
+
await expect(request("/api/boom")).rejects.toMatchObject({
|
|
91
|
+
status: 401,
|
|
92
|
+
message: "Unauthorized",
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
it("extracts error from {error} body", async () => {
|
|
96
|
+
server.use(http.get(`${TEST_BASE_URL}/api/boom`, () => HttpResponse.json({ error: "Rate limit exceeded" }, { status: 429 })));
|
|
97
|
+
await expect(request("/api/boom")).rejects.toMatchObject({
|
|
98
|
+
status: 429,
|
|
99
|
+
message: "Rate limit exceeded",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("attaches Retry-After header on 429", async () => {
|
|
103
|
+
server.use(http.get(`${TEST_BASE_URL}/api/limited`, () => HttpResponse.json({ error: "slow down" }, { status: 429, headers: { "Retry-After": "60" } })));
|
|
104
|
+
await expect(request("/api/limited")).rejects.toMatchObject({
|
|
105
|
+
status: 429,
|
|
106
|
+
retryAfter: "60",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it("supports rawBody PUT (binary)", async () => {
|
|
110
|
+
let seenCT = "";
|
|
111
|
+
let seenMethod = "";
|
|
112
|
+
server.use(http.put(`${TEST_BASE_URL}/api/uploads/u1`, ({ request: req }) => {
|
|
113
|
+
seenCT = req.headers.get("content-type") ?? "";
|
|
114
|
+
seenMethod = req.method;
|
|
115
|
+
return HttpResponse.json({ success: true });
|
|
116
|
+
}));
|
|
117
|
+
const bytes = new Uint8Array([1, 2, 3]);
|
|
118
|
+
const result = await request("/api/uploads/u1", {
|
|
119
|
+
method: "PUT",
|
|
120
|
+
rawBody: bytes,
|
|
121
|
+
contentType: "application/octet-stream",
|
|
122
|
+
});
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(seenCT).toContain("application/octet-stream");
|
|
125
|
+
expect(seenMethod).toBe("PUT");
|
|
126
|
+
});
|
|
127
|
+
});
|