@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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +379 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +21 -0
  5. package/dist/client.d.ts +18 -0
  6. package/dist/client.js +90 -0
  7. package/dist/client.test.d.ts +1 -0
  8. package/dist/client.test.js +127 -0
  9. package/dist/commands/configure.d.ts +24 -0
  10. package/dist/commands/configure.js +103 -0
  11. package/dist/commands/configure.test.d.ts +1 -0
  12. package/dist/commands/configure.test.js +96 -0
  13. package/dist/commands/device/connect.d.ts +2 -0
  14. package/dist/commands/device/connect.js +84 -0
  15. package/dist/commands/device/doctor.d.ts +34 -0
  16. package/dist/commands/device/doctor.js +421 -0
  17. package/dist/commands/device/doctor.test.d.ts +1 -0
  18. package/dist/commands/device/doctor.test.js +89 -0
  19. package/dist/commands/device/index.d.ts +2 -0
  20. package/dist/commands/device/index.js +16 -0
  21. package/dist/commands/device/list.d.ts +20 -0
  22. package/dist/commands/device/list.js +40 -0
  23. package/dist/commands/device/list.test.d.ts +1 -0
  24. package/dist/commands/device/list.test.js +38 -0
  25. package/dist/commands/device/logs.d.ts +34 -0
  26. package/dist/commands/device/logs.js +160 -0
  27. package/dist/commands/device/logs.test.d.ts +1 -0
  28. package/dist/commands/device/logs.test.js +70 -0
  29. package/dist/commands/device/paths.d.ts +12 -0
  30. package/dist/commands/device/paths.js +40 -0
  31. package/dist/commands/device/ping.d.ts +21 -0
  32. package/dist/commands/device/ping.js +66 -0
  33. package/dist/commands/device/ping.test.d.ts +1 -0
  34. package/dist/commands/device/ping.test.js +68 -0
  35. package/dist/commands/device/register.d.ts +23 -0
  36. package/dist/commands/device/register.js +83 -0
  37. package/dist/commands/device/reset.d.ts +30 -0
  38. package/dist/commands/device/reset.js +67 -0
  39. package/dist/commands/device/reset.test.d.ts +1 -0
  40. package/dist/commands/device/reset.test.js +69 -0
  41. package/dist/commands/device/setup.d.ts +25 -0
  42. package/dist/commands/device/setup.js +105 -0
  43. package/dist/commands/device/setup.test.d.ts +1 -0
  44. package/dist/commands/device/setup.test.js +64 -0
  45. package/dist/commands/device/status.d.ts +16 -0
  46. package/dist/commands/device/status.js +20 -0
  47. package/dist/commands/device/unregister.d.ts +14 -0
  48. package/dist/commands/device/unregister.js +60 -0
  49. package/dist/commands/health/check.d.ts +5 -0
  50. package/dist/commands/health/check.js +20 -0
  51. package/dist/commands/health/check.test.d.ts +1 -0
  52. package/dist/commands/health/check.test.js +26 -0
  53. package/dist/commands/health/index.d.ts +2 -0
  54. package/dist/commands/health/index.js +7 -0
  55. package/dist/commands/project/index.d.ts +2 -0
  56. package/dist/commands/project/index.js +7 -0
  57. package/dist/commands/project/list.d.ts +7 -0
  58. package/dist/commands/project/list.js +20 -0
  59. package/dist/commands/project/list.test.d.ts +1 -0
  60. package/dist/commands/project/list.test.js +28 -0
  61. package/dist/commands/schema.d.ts +27 -0
  62. package/dist/commands/schema.js +146 -0
  63. package/dist/commands/schema.test.d.ts +1 -0
  64. package/dist/commands/schema.test.js +38 -0
  65. package/dist/commands/session/create.d.ts +78 -0
  66. package/dist/commands/session/create.js +89 -0
  67. package/dist/commands/session/get.d.ts +11 -0
  68. package/dist/commands/session/get.js +30 -0
  69. package/dist/commands/session/index.d.ts +2 -0
  70. package/dist/commands/session/index.js +8 -0
  71. package/dist/commands/session/session.test.d.ts +1 -0
  72. package/dist/commands/session/session.test.js +128 -0
  73. package/dist/commands/trigger/case/get.d.ts +14 -0
  74. package/dist/commands/trigger/case/get.js +33 -0
  75. package/dist/commands/trigger/case/index.d.ts +2 -0
  76. package/dist/commands/trigger/case/index.js +8 -0
  77. package/dist/commands/trigger/case/run.d.ts +58 -0
  78. package/dist/commands/trigger/case/run.js +81 -0
  79. package/dist/commands/trigger/index.d.ts +2 -0
  80. package/dist/commands/trigger/index.js +8 -0
  81. package/dist/commands/trigger/plan/get.d.ts +14 -0
  82. package/dist/commands/trigger/plan/get.js +33 -0
  83. package/dist/commands/trigger/plan/index.d.ts +2 -0
  84. package/dist/commands/trigger/plan/index.js +8 -0
  85. package/dist/commands/trigger/plan/run.d.ts +33 -0
  86. package/dist/commands/trigger/plan/run.js +63 -0
  87. package/dist/commands/trigger/trigger.test.d.ts +1 -0
  88. package/dist/commands/trigger/trigger.test.js +107 -0
  89. package/dist/commands/upload/create.d.ts +32 -0
  90. package/dist/commands/upload/create.js +65 -0
  91. package/dist/commands/upload/get.d.ts +10 -0
  92. package/dist/commands/upload/get.js +30 -0
  93. package/dist/commands/upload/index.d.ts +2 -0
  94. package/dist/commands/upload/index.js +11 -0
  95. package/dist/commands/upload/list.d.ts +15 -0
  96. package/dist/commands/upload/list.js +31 -0
  97. package/dist/commands/upload/put.d.ts +24 -0
  98. package/dist/commands/upload/put.js +62 -0
  99. package/dist/commands/upload/submit.d.ts +19 -0
  100. package/dist/commands/upload/submit.js +36 -0
  101. package/dist/commands/upload/upload.test.d.ts +1 -0
  102. package/dist/commands/upload/upload.test.js +133 -0
  103. package/dist/config.d.ts +7 -0
  104. package/dist/config.js +36 -0
  105. package/dist/desktop-server/agent-sync.d.ts +19 -0
  106. package/dist/desktop-server/agent-sync.js +141 -0
  107. package/dist/desktop-server/agent-sync.test.d.ts +1 -0
  108. package/dist/desktop-server/agent-sync.test.js +130 -0
  109. package/dist/desktop-server/setup.d.ts +19 -0
  110. package/dist/desktop-server/setup.js +200 -0
  111. package/dist/errors.d.ts +2 -0
  112. package/dist/errors.js +71 -0
  113. package/dist/errors.test.d.ts +1 -0
  114. package/dist/errors.test.js +88 -0
  115. package/dist/index.d.ts +19 -0
  116. package/dist/index.js +17 -0
  117. package/dist/mutate.d.ts +13 -0
  118. package/dist/mutate.js +25 -0
  119. package/dist/mutate.test.d.ts +1 -0
  120. package/dist/mutate.test.js +49 -0
  121. package/dist/output.d.ts +5 -0
  122. package/dist/output.js +23 -0
  123. package/dist/output.test.d.ts +1 -0
  124. package/dist/output.test.js +42 -0
  125. package/dist/test-setup.d.ts +1 -0
  126. package/dist/test-setup.js +13 -0
  127. package/dist/test-utils.d.ts +4 -0
  128. package/dist/test-utils.js +5 -0
  129. package/dist/types.d.ts +121 -0
  130. package/dist/types.js +2 -0
  131. package/dist/validate.d.ts +14 -0
  132. package/dist/validate.js +103 -0
  133. package/dist/validate.test.d.ts +1 -0
  134. package/dist/validate.test.js +96 -0
  135. package/package.json +72 -0
@@ -0,0 +1,421 @@
1
+ import { defineCommand } from "citty";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { platform } from "node:os";
5
+ import { WebSocket } from "ws";
6
+ import { getBaseUrl, request } from "../../client.js";
7
+ import { formatSuccess, printResult } from "../../output.js";
8
+ import { loadConfig } from "../../config.js";
9
+ import { readCachedManifest } from "../../desktop-server/agent-sync.js";
10
+ import { installedPythonTag, PYTHON_RELEASE, PYTHON_VERSION } from "../../desktop-server/setup.js";
11
+ import { getAgentRequirementsPath, getAgentScriptPath, getDepsMarkerPath, getDeviceConfigPath, getEmbeddedPythonBinary, getPythonDir, getPythonMarkerPath, } from "./paths.js";
12
+ function redactKey(value) {
13
+ if (value.length <= 8)
14
+ return "***";
15
+ return `${value.slice(0, 4)}…${value.slice(-4)}`;
16
+ }
17
+ function checkApiKey() {
18
+ // Match the runtime precedence in client.ts: config file > env > default.
19
+ const config = loadConfig();
20
+ const source = config.apiKey ? "config file" : process.env["AXIMO_API_KEY"] ? "env" : null;
21
+ const value = config.apiKey ?? process.env["AXIMO_API_KEY"];
22
+ if (!value || !source) {
23
+ return {
24
+ name: "API key",
25
+ status: "fail",
26
+ message: "not set",
27
+ hint: "Run: aximo configure (or export AXIMO_API_KEY=...)",
28
+ };
29
+ }
30
+ return {
31
+ name: "API key",
32
+ status: "pass",
33
+ message: `set via ${source} (${redactKey(value)})`,
34
+ };
35
+ }
36
+ function checkBaseUrl() {
37
+ const config = loadConfig();
38
+ const resolved = getBaseUrl();
39
+ if (config.baseUrl) {
40
+ return { name: "base URL", status: "pass", message: `${resolved} (from config file)` };
41
+ }
42
+ if (process.env["AXIMO_BASE_URL"]) {
43
+ return { name: "base URL", status: "pass", message: `${resolved} (from env)` };
44
+ }
45
+ return { name: "base URL", status: "info", message: `default: ${resolved}` };
46
+ }
47
+ function checkProxyEnv() {
48
+ const vars = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"];
49
+ const present = vars.filter((v) => process.env[v]);
50
+ if (present.length === 0) {
51
+ return { name: "proxy", status: "info", message: "no proxy env vars set" };
52
+ }
53
+ return { name: "proxy", status: "info", message: `proxy vars present: ${present.join(", ")}` };
54
+ }
55
+ async function checkApiHealth(deps) {
56
+ try {
57
+ await request("/api/health", {}, deps ?? {});
58
+ return { name: "aximo API health", status: "pass", message: "reachable" };
59
+ }
60
+ catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ return {
63
+ name: "aximo API health",
64
+ status: "fail",
65
+ message,
66
+ hint: "Check AXIMO_BASE_URL and network connectivity",
67
+ };
68
+ }
69
+ }
70
+ function checkDeviceConfig() {
71
+ const configPath = getDeviceConfigPath();
72
+ if (!existsSync(configPath)) {
73
+ return {
74
+ check: {
75
+ name: "device registered",
76
+ status: "warn",
77
+ message: "no local device config",
78
+ hint: 'Run: aximo device register --name "My PC"',
79
+ },
80
+ };
81
+ }
82
+ try {
83
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
84
+ if (!config.deviceId) {
85
+ return {
86
+ check: {
87
+ name: "device registered",
88
+ status: "fail",
89
+ message: "device.json missing deviceId",
90
+ hint: "Re-register: aximo device register --name ...",
91
+ },
92
+ };
93
+ }
94
+ return {
95
+ check: {
96
+ name: "device registered",
97
+ status: "pass",
98
+ message: `${config.name ?? "(unnamed)"} · ${config.deviceId}`,
99
+ },
100
+ config,
101
+ };
102
+ }
103
+ catch (err) {
104
+ return {
105
+ check: {
106
+ name: "device registered",
107
+ status: "fail",
108
+ message: `device.json unreadable: ${err instanceof Error ? err.message : String(err)}`,
109
+ hint: "Delete the file and re-register",
110
+ },
111
+ };
112
+ }
113
+ }
114
+ function checkEmbeddedPython() {
115
+ if (!existsSync(getPythonDir())) {
116
+ return {
117
+ name: "embedded Python",
118
+ status: "fail",
119
+ message: "not installed",
120
+ hint: "Run: aximo device setup",
121
+ };
122
+ }
123
+ const binary = getEmbeddedPythonBinary();
124
+ if (!existsSync(binary)) {
125
+ return {
126
+ name: "embedded Python",
127
+ status: "fail",
128
+ message: `binary missing at ${binary}`,
129
+ hint: "Run: aximo device reset --python && aximo device setup",
130
+ };
131
+ }
132
+ const markerPath = getPythonMarkerPath();
133
+ const installed = existsSync(markerPath) ? readFileSync(markerPath, "utf-8").trim() : "";
134
+ const expected = installedPythonTag();
135
+ if (installed !== expected) {
136
+ return {
137
+ name: "embedded Python",
138
+ status: "warn",
139
+ message: `installed ${installed || "unknown"}, expected ${expected}`,
140
+ hint: "Run: aximo device setup (to upgrade)",
141
+ };
142
+ }
143
+ const result = spawnSync(binary, ["--version"], { encoding: "utf-8" });
144
+ if (result.status !== 0) {
145
+ return {
146
+ name: "embedded Python",
147
+ status: "fail",
148
+ message: `binary does not run: ${(result.stderr || result.stdout || "").trim()}`,
149
+ hint: "Run: aximo device reset --python && aximo device setup",
150
+ };
151
+ }
152
+ return {
153
+ name: "embedded Python",
154
+ status: "pass",
155
+ message: `${(result.stdout || result.stderr).trim()} (${PYTHON_VERSION}+${PYTHON_RELEASE})`,
156
+ };
157
+ }
158
+ function checkPythonDeps() {
159
+ if (!existsSync(getDepsMarkerPath())) {
160
+ return {
161
+ name: "python dependencies",
162
+ status: "warn",
163
+ message: "not yet installed",
164
+ hint: "Run: aximo device setup",
165
+ };
166
+ }
167
+ const binary = getEmbeddedPythonBinary();
168
+ if (!existsSync(binary)) {
169
+ return { name: "python dependencies", status: "fail", message: "python binary missing" };
170
+ }
171
+ const result = spawnSync(binary, ["-c", "import pyautogui, pyperclip, websockets, PIL"], {
172
+ encoding: "utf-8",
173
+ });
174
+ if (result.status !== 0) {
175
+ return {
176
+ name: "python dependencies",
177
+ status: "fail",
178
+ message: (result.stderr || result.stdout || "").trim() || "import failed",
179
+ hint: "Run: aximo device reset --deps && aximo device setup",
180
+ };
181
+ }
182
+ return {
183
+ name: "python dependencies",
184
+ status: "pass",
185
+ message: "pyautogui, pyperclip, websockets, PIL importable",
186
+ };
187
+ }
188
+ function checkAgentFiles() {
189
+ const scriptPath = getAgentScriptPath();
190
+ const requirementsPath = getAgentRequirementsPath();
191
+ if (!existsSync(scriptPath) || !existsSync(requirementsPath)) {
192
+ return {
193
+ name: "agent files",
194
+ status: "warn",
195
+ message: "agent script not yet synced",
196
+ hint: "Run: aximo device setup",
197
+ };
198
+ }
199
+ const local = readCachedManifest();
200
+ if (!local) {
201
+ return {
202
+ name: "agent files",
203
+ status: "warn",
204
+ message: "agent manifest missing or invalid",
205
+ hint: "Run: aximo device setup",
206
+ };
207
+ }
208
+ return {
209
+ name: "agent files",
210
+ status: "pass",
211
+ message: `version ${local.version}`,
212
+ };
213
+ }
214
+ async function checkAgentUpToDate(deps) {
215
+ const local = readCachedManifest();
216
+ try {
217
+ const server = await request("/api/devices/agent/manifest", {}, deps ?? {});
218
+ if (!local) {
219
+ return {
220
+ name: "agent up to date",
221
+ status: "warn",
222
+ message: `local cache missing; server has version ${server.version}`,
223
+ hint: "Run: aximo device setup",
224
+ };
225
+ }
226
+ if (local.version === server.version) {
227
+ return {
228
+ name: "agent up to date",
229
+ status: "pass",
230
+ message: `version ${server.version}`,
231
+ };
232
+ }
233
+ return {
234
+ name: "agent up to date",
235
+ status: "warn",
236
+ message: `local ${local.version}, server ${server.version}`,
237
+ hint: "Run: aximo device setup (to fetch latest)",
238
+ };
239
+ }
240
+ catch (err) {
241
+ return {
242
+ name: "agent up to date",
243
+ status: "fail",
244
+ message: err instanceof Error ? err.message : String(err),
245
+ hint: "Check AXIMO_BASE_URL and AXIMO_API_KEY",
246
+ };
247
+ }
248
+ }
249
+ function relayUrlFromBase() {
250
+ return `${getBaseUrl().replace(/^http/, "ws")}/devices/tunnel`;
251
+ }
252
+ const RELAY_CHECK_NAME = "relay handshake";
253
+ function checkRelayReachable() {
254
+ return new Promise((resolve) => {
255
+ const url = relayUrlFromBase();
256
+ let settled = false;
257
+ const ws = new WebSocket(url, { handshakeTimeout: 5_000 });
258
+ const done = (check) => {
259
+ if (settled)
260
+ return;
261
+ settled = true;
262
+ try {
263
+ ws.close();
264
+ }
265
+ catch {
266
+ // ignore
267
+ }
268
+ resolve(check);
269
+ };
270
+ const timer = setTimeout(() => {
271
+ done({
272
+ name: RELAY_CHECK_NAME,
273
+ status: "fail",
274
+ message: `timed out connecting to ${url}`,
275
+ hint: "Check firewall / proxy for outbound wss:// on 443",
276
+ });
277
+ }, 5_000);
278
+ ws.on("open", () => {
279
+ clearTimeout(timer);
280
+ done({
281
+ name: RELAY_CHECK_NAME,
282
+ status: "pass",
283
+ message: `WebSocket handshake to ${url} succeeded (device auth not verified)`,
284
+ });
285
+ });
286
+ ws.on("error", (err) => {
287
+ clearTimeout(timer);
288
+ done({
289
+ name: RELAY_CHECK_NAME,
290
+ status: "fail",
291
+ message: `${url}: ${err instanceof Error ? err.message : String(err)}`,
292
+ hint: "Check AXIMO_BASE_URL and firewall rules for outbound wss://",
293
+ });
294
+ });
295
+ });
296
+ }
297
+ async function checkCurrentStatus(deviceId, deps) {
298
+ const name = "device status";
299
+ try {
300
+ const res = await request("/api/devices/user", {}, deps ?? {});
301
+ const device = res.devices?.find((d) => d.id === deviceId);
302
+ if (!device) {
303
+ return {
304
+ name,
305
+ status: "warn",
306
+ message: "device not listed for current API key",
307
+ hint: "Token may be from another account; re-register",
308
+ };
309
+ }
310
+ if (device.status === "online") {
311
+ return { name, status: "pass", message: `online · last seen ${device.lastSeenAt ?? "now"}` };
312
+ }
313
+ return {
314
+ name,
315
+ status: "warn",
316
+ message: `status: ${device.status}`,
317
+ hint: "Run: aximo device connect",
318
+ };
319
+ }
320
+ catch (err) {
321
+ return {
322
+ name,
323
+ status: "fail",
324
+ message: err instanceof Error ? err.message : String(err),
325
+ };
326
+ }
327
+ }
328
+ function checkPlatformExtras() {
329
+ const p = platform();
330
+ if (p === "linux") {
331
+ const needs = ["scrot", "xclip"];
332
+ const found = needs.filter((cmd) => spawnSync("which", [cmd]).status === 0);
333
+ if (found.length === needs.length) {
334
+ return [{ name: "linux tools", status: "pass", message: `${needs.join(", ")} installed` }];
335
+ }
336
+ const missing = needs.filter((c) => !found.includes(c));
337
+ return [
338
+ {
339
+ name: "linux tools",
340
+ status: "warn",
341
+ message: `missing: ${missing.join(", ")}`,
342
+ hint: "Install via your package manager (e.g. apt install scrot xclip)",
343
+ },
344
+ ];
345
+ }
346
+ if (p === "darwin") {
347
+ return [
348
+ {
349
+ name: "macos permissions",
350
+ status: "info",
351
+ message: "Accessibility + Screen Recording must be granted manually",
352
+ hint: "System Settings → Privacy & Security → Accessibility / Screen Recording",
353
+ },
354
+ ];
355
+ }
356
+ return [];
357
+ }
358
+ function logDirUsage() {
359
+ const pythonDir = getPythonDir();
360
+ try {
361
+ if (existsSync(pythonDir)) {
362
+ const stat = statSync(pythonDir);
363
+ if (stat.isDirectory()) {
364
+ return { name: "install dir", status: "info", message: pythonDir };
365
+ }
366
+ }
367
+ }
368
+ catch {
369
+ // ignore
370
+ }
371
+ return { name: "install dir", status: "info", message: pythonDir };
372
+ }
373
+ export async function runDeviceDoctor(options = {}, deps) {
374
+ const checks = [];
375
+ checks.push(checkApiKey());
376
+ checks.push(checkBaseUrl());
377
+ checks.push(checkProxyEnv());
378
+ if (!options.skipApi && checks[0]?.status !== "fail") {
379
+ checks.push(await checkApiHealth(deps));
380
+ }
381
+ const { check: configCheck, config } = checkDeviceConfig();
382
+ checks.push(configCheck);
383
+ checks.push(checkEmbeddedPython());
384
+ checks.push(checkPythonDeps());
385
+ checks.push(checkAgentFiles());
386
+ checks.push(...checkPlatformExtras());
387
+ checks.push(logDirUsage());
388
+ if (!options.skipApi && checks[0]?.status !== "fail") {
389
+ checks.push(await checkAgentUpToDate(deps));
390
+ }
391
+ if (!options.skipRelay) {
392
+ checks.push(await checkRelayReachable());
393
+ }
394
+ if (config?.deviceId && !options.skipApi && checks[0]?.status !== "fail") {
395
+ checks.push(await checkCurrentStatus(config.deviceId, deps));
396
+ }
397
+ const summary = { pass: 0, warn: 0, fail: 0, info: 0 };
398
+ for (const c of checks)
399
+ summary[c.status]++;
400
+ return formatSuccess({ checks, summary });
401
+ }
402
+ export default defineCommand({
403
+ meta: {
404
+ name: "doctor",
405
+ description: "Diagnose your BYOD setup (env, Python, relay, device status)",
406
+ },
407
+ args: {
408
+ "skip-relay": { type: "boolean", description: "Skip WebSocket reachability check" },
409
+ "skip-api": { type: "boolean", description: "Skip Aximo API calls (offline check only)" },
410
+ },
411
+ async run({ args }) {
412
+ const result = await runDeviceDoctor({
413
+ skipRelay: Boolean(args["skip-relay"]),
414
+ skipApi: Boolean(args["skip-api"]),
415
+ });
416
+ printResult(result);
417
+ if (result.ok && result.data.summary.fail > 0) {
418
+ process.exitCode = 1;
419
+ }
420
+ },
421
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { server, http, HttpResponse, TEST_BASE_URL } from "../../test-utils.js";
6
+ import { runDeviceDoctor } from "./doctor.js";
7
+ let tempHome;
8
+ beforeAll(() => {
9
+ tempHome = mkdtempSync(join(tmpdir(), "aximo-doctor-"));
10
+ process.env["AXIMO_HOME"] = tempHome;
11
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
12
+ server.listen();
13
+ });
14
+ afterAll(() => {
15
+ delete process.env["AXIMO_HOME"];
16
+ server.close();
17
+ rmSync(tempHome, { recursive: true, force: true });
18
+ });
19
+ beforeEach(() => {
20
+ rmSync(tempHome, { recursive: true, force: true });
21
+ mkdirSync(tempHome, { recursive: true });
22
+ });
23
+ afterEach(() => server.resetHandlers());
24
+ function findCheck(checks, name) {
25
+ return checks.find((c) => c.name === name);
26
+ }
27
+ describe("runDeviceDoctor", () => {
28
+ it("fails the API key check when AXIMO_API_KEY is unset", async () => {
29
+ delete process.env["AXIMO_API_KEY"];
30
+ const result = await runDeviceDoctor({ skipRelay: true, skipApi: true });
31
+ expect(result.ok).toBe(true);
32
+ if (!result.ok)
33
+ return;
34
+ const apiKey = findCheck(result.data.checks, "API key");
35
+ expect(apiKey?.status).toBe("fail");
36
+ expect(result.data.summary.fail).toBeGreaterThanOrEqual(1);
37
+ });
38
+ it("passes the API key check when the env is set and warns on missing local device config", async () => {
39
+ process.env["AXIMO_API_KEY"] = "test-key";
40
+ const result = await runDeviceDoctor({ skipRelay: true, skipApi: true });
41
+ expect(result.ok).toBe(true);
42
+ if (!result.ok)
43
+ return;
44
+ expect(findCheck(result.data.checks, "API key")?.status).toBe("pass");
45
+ expect(findCheck(result.data.checks, "device registered")?.status).toBe("warn");
46
+ });
47
+ it("passes the API health check when /api/health responds", async () => {
48
+ process.env["AXIMO_API_KEY"] = "test-key";
49
+ server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ status: "ok" })), http.get(`${TEST_BASE_URL}/api/devices/user`, () => HttpResponse.json({ devices: [] })), http.get(`${TEST_BASE_URL}/api/devices/agent/manifest`, () => HttpResponse.json({
50
+ manifestVersion: 2,
51
+ version: "v1",
52
+ script: { sha256: "s", size: 0 },
53
+ requirements: { sha256: "r", size: 0 },
54
+ })));
55
+ const result = await runDeviceDoctor({ skipRelay: true });
56
+ expect(result.ok).toBe(true);
57
+ if (!result.ok)
58
+ return;
59
+ expect(findCheck(result.data.checks, "aximo API health")?.status).toBe("pass");
60
+ });
61
+ it("reports device status when a local config matches a listed device", async () => {
62
+ process.env["AXIMO_API_KEY"] = "test-key";
63
+ const deviceId = "dev-1";
64
+ writeFileSync(join(tempHome, "device.json"), JSON.stringify({ deviceId, deviceToken: "t", name: "Test PC" }));
65
+ server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ status: "ok" })), http.get(`${TEST_BASE_URL}/api/devices/user`, () => HttpResponse.json({
66
+ devices: [{ id: deviceId, name: "Test PC", status: "online", lastSeenAt: "now" }],
67
+ })), http.get(`${TEST_BASE_URL}/api/devices/agent/manifest`, () => HttpResponse.json({
68
+ manifestVersion: 2,
69
+ version: "v1",
70
+ script: { sha256: "s", size: 0 },
71
+ requirements: { sha256: "r", size: 0 },
72
+ })));
73
+ const result = await runDeviceDoctor({ skipRelay: true });
74
+ expect(result.ok).toBe(true);
75
+ if (!result.ok)
76
+ return;
77
+ const status = findCheck(result.data.checks, "device status");
78
+ expect(status?.status).toBe("pass");
79
+ expect(status?.message).toContain("online");
80
+ });
81
+ it("flags agent-files as warn when no local cache exists", async () => {
82
+ process.env["AXIMO_API_KEY"] = "test-key";
83
+ const result = await runDeviceDoctor({ skipRelay: true, skipApi: true });
84
+ expect(result.ok).toBe(true);
85
+ if (!result.ok)
86
+ return;
87
+ expect(findCheck(result.data.checks, "agent files")?.status).toBe("warn");
88
+ });
89
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { defineCommand } from "citty";
2
+ export default defineCommand({
3
+ meta: { name: "device", description: "Device commands" },
4
+ subCommands: {
5
+ list: () => import("./list.js").then((m) => m.default),
6
+ register: () => import("./register.js").then((m) => m.default),
7
+ connect: () => import("./connect.js").then((m) => m.default),
8
+ status: () => import("./status.js").then((m) => m.default),
9
+ unregister: () => import("./unregister.js").then((m) => m.default),
10
+ setup: () => import("./setup.js").then((m) => m.default),
11
+ doctor: () => import("./doctor.js").then((m) => m.default),
12
+ logs: () => import("./logs.js").then((m) => m.default),
13
+ reset: () => import("./reset.js").then((m) => m.default),
14
+ ping: () => import("./ping.js").then((m) => m.default),
15
+ },
16
+ });
@@ -0,0 +1,20 @@
1
+ import type { Envelope, Device } from "../../types.js";
2
+ import { type ClientDeps } from "../../client.js";
3
+ export interface DeviceListOptions {
4
+ platform?: string | undefined;
5
+ availableOnly?: boolean | undefined;
6
+ }
7
+ export declare function runDeviceList(options?: DeviceListOptions, deps?: ClientDeps): Promise<Envelope<{
8
+ devices: Device[];
9
+ }>>;
10
+ declare const _default: import("citty").CommandDef<{
11
+ readonly platform: {
12
+ readonly type: "string";
13
+ readonly description: "ANDROID | IOS";
14
+ };
15
+ readonly "available-only": {
16
+ readonly type: "boolean";
17
+ readonly description: "Only available devices";
18
+ };
19
+ }>;
20
+ export default _default;
@@ -0,0 +1,40 @@
1
+ import { defineCommand } from "citty";
2
+ import { formatSuccess, printResult, validationError } from "../../output.js";
3
+ import { handleApiError } from "../../errors.js";
4
+ import { request } from "../../client.js";
5
+ import { parsePlatform } from "../../validate.js";
6
+ export async function runDeviceList(options = {}, deps) {
7
+ let platform;
8
+ try {
9
+ platform = parsePlatform(options.platform);
10
+ }
11
+ catch (err) {
12
+ return validationError(err);
13
+ }
14
+ try {
15
+ const res = await request("/api/devices", {
16
+ query: {
17
+ platform,
18
+ availableOnly: options.availableOnly,
19
+ },
20
+ }, deps ?? {});
21
+ const devices = res.devices ?? [];
22
+ return formatSuccess({ devices }, { total: devices.length });
23
+ }
24
+ catch (err) {
25
+ return handleApiError(err);
26
+ }
27
+ }
28
+ export default defineCommand({
29
+ meta: { name: "list", description: "List Aximo devices" },
30
+ args: {
31
+ platform: { type: "string", description: "ANDROID | IOS" },
32
+ "available-only": { type: "boolean", description: "Only available devices" },
33
+ },
34
+ async run({ args }) {
35
+ printResult(await runDeviceList({
36
+ platform: args.platform,
37
+ availableOnly: args["available-only"],
38
+ }));
39
+ },
40
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
2
+ import { server, http, HttpResponse, TEST_BASE_URL } from "../../test-utils.js";
3
+ import { runDeviceList } from "./list.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
+ describe("runDeviceList", () => {
12
+ it("forwards platform + availableOnly as query", async () => {
13
+ let seenUrl = "";
14
+ server.use(http.get(`${TEST_BASE_URL}/api/devices`, ({ request: req }) => {
15
+ seenUrl = req.url;
16
+ return HttpResponse.json({ devices: [{ name: "d1" }] });
17
+ }));
18
+ const result = await runDeviceList({ platform: "IOS", availableOnly: true });
19
+ expect(result.ok).toBe(true);
20
+ expect(seenUrl).toContain("platform=IOS");
21
+ expect(seenUrl).toContain("availableOnly=true");
22
+ });
23
+ it("rejects invalid platform with validation_error", async () => {
24
+ const result = await runDeviceList({ platform: "WINDOWS" });
25
+ expect(result.ok).toBe(false);
26
+ if (!result.ok)
27
+ expect(result.error.code).toBe("validation_error");
28
+ });
29
+ it("maps 429 to rate_limited", async () => {
30
+ server.use(http.get(`${TEST_BASE_URL}/api/devices`, () => HttpResponse.json({ error: "Rate limit exceeded" }, { status: 429 })));
31
+ const result = await runDeviceList();
32
+ expect(result.ok).toBe(false);
33
+ if (!result.ok) {
34
+ expect(result.error.code).toBe("rate_limited");
35
+ expect(result.error.retryable).toBe(true);
36
+ }
37
+ });
38
+ });
@@ -0,0 +1,34 @@
1
+ import type { Envelope } from "../../types.js";
2
+ export interface LogsResult {
3
+ path: string;
4
+ lines?: string[];
5
+ truncated?: boolean;
6
+ cleared?: boolean;
7
+ }
8
+ export interface RunDeviceLogsOptions {
9
+ lines?: number;
10
+ path?: boolean;
11
+ clear?: boolean;
12
+ }
13
+ export declare function runDeviceLogs(options?: RunDeviceLogsOptions): Envelope<LogsResult>;
14
+ declare const _default: import("citty").CommandDef<{
15
+ readonly follow: {
16
+ readonly type: "boolean";
17
+ readonly alias: "f";
18
+ readonly description: "Stream new log lines as they are written";
19
+ };
20
+ readonly lines: {
21
+ readonly type: "string";
22
+ readonly alias: "n";
23
+ readonly description: "Number of lines to show (default 50)";
24
+ };
25
+ readonly path: {
26
+ readonly type: "boolean";
27
+ readonly description: "Print log file path and exit";
28
+ };
29
+ readonly clear: {
30
+ readonly type: "boolean";
31
+ readonly description: "Truncate the log file";
32
+ };
33
+ }>;
34
+ export default _default;