@autifyhq/aximo-cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +19 -1
  2. package/dist/commands/device/device-config.d.ts +2 -2
  3. package/dist/commands/device/device-config.js +5 -8
  4. package/dist/commands/device/index.js +2 -1
  5. package/dist/commands/device/paths.d.ts +4 -0
  6. package/dist/commands/device/paths.js +12 -0
  7. package/dist/commands/device/reset.d.ts +1 -0
  8. package/dist/commands/device/reset.js +18 -3
  9. package/dist/commands/device/reset.test.js +13 -0
  10. package/dist/commands/device/testbox/box.d.ts +90 -0
  11. package/dist/commands/device/testbox/box.js +379 -0
  12. package/dist/commands/device/testbox/box.test.d.ts +1 -0
  13. package/dist/commands/device/testbox/box.test.js +815 -0
  14. package/dist/commands/device/testbox/container.d.ts +58 -0
  15. package/dist/commands/device/testbox/container.js +289 -0
  16. package/dist/commands/device/testbox/docker.d.ts +31 -0
  17. package/dist/commands/device/testbox/docker.js +78 -0
  18. package/dist/commands/device/testbox/docker.test.d.ts +1 -0
  19. package/dist/commands/device/testbox/docker.test.js +82 -0
  20. package/dist/commands/device/testbox/down.d.ts +7 -0
  21. package/dist/commands/device/testbox/down.js +14 -0
  22. package/dist/commands/device/testbox/errors.d.ts +4 -0
  23. package/dist/commands/device/testbox/errors.js +7 -0
  24. package/dist/commands/device/testbox/fake-docker.d.ts +25 -0
  25. package/dist/commands/device/testbox/fake-docker.js +35 -0
  26. package/dist/commands/device/testbox/index.d.ts +2 -0
  27. package/dist/commands/device/testbox/index.js +16 -0
  28. package/dist/commands/device/testbox/logs.d.ts +15 -0
  29. package/dist/commands/device/testbox/logs.js +43 -0
  30. package/dist/commands/device/testbox/prune.d.ts +11 -0
  31. package/dist/commands/device/testbox/prune.js +17 -0
  32. package/dist/commands/device/testbox/registration.d.ts +9 -0
  33. package/dist/commands/device/testbox/registration.js +56 -0
  34. package/dist/commands/device/testbox/reset.d.ts +7 -0
  35. package/dist/commands/device/testbox/reset.js +14 -0
  36. package/dist/commands/device/testbox/result.d.ts +8 -0
  37. package/dist/commands/device/testbox/result.js +23 -0
  38. package/dist/commands/device/testbox/result.test.d.ts +1 -0
  39. package/dist/commands/device/testbox/result.test.js +34 -0
  40. package/dist/commands/device/testbox/self-command.d.ts +12 -0
  41. package/dist/commands/device/testbox/self-command.js +36 -0
  42. package/dist/commands/device/testbox/self-command.test.d.ts +1 -0
  43. package/dist/commands/device/testbox/self-command.test.js +58 -0
  44. package/dist/commands/device/testbox/status.d.ts +7 -0
  45. package/dist/commands/device/testbox/status.js +11 -0
  46. package/dist/commands/device/testbox/up.d.ts +47 -0
  47. package/dist/commands/device/testbox/up.js +76 -0
  48. package/dist/commands/device/testbox/up.test.d.ts +1 -0
  49. package/dist/commands/device/testbox/up.test.js +52 -0
  50. package/dist/commands/device/testbox/url.d.ts +7 -0
  51. package/dist/commands/device/testbox/url.js +20 -0
  52. package/dist/commands/device/user-devices.d.ts +4 -0
  53. package/dist/commands/device/user-devices.js +18 -0
  54. package/dist/commands/mcp/server.d.ts +2 -2
  55. package/dist/commands/mcp/server.test.js +6 -2
  56. package/dist/commands/mcp/testbox-tools.test.d.ts +1 -0
  57. package/dist/commands/mcp/testbox-tools.test.js +278 -0
  58. package/dist/commands/mcp/tools.d.ts +8 -1
  59. package/dist/commands/mcp/tools.js +151 -18
  60. package/dist/commands/mcp/tools.test.js +27 -2
  61. package/dist/commands/schema.js +4 -0
  62. package/dist/commands/session/cancel.d.ts +20 -0
  63. package/dist/commands/session/cancel.js +39 -0
  64. package/dist/commands/session/index.js +1 -0
  65. package/dist/commands/session/session.test.js +38 -0
  66. package/dist/errors.d.ts +2 -0
  67. package/dist/errors.js +5 -0
  68. package/dist/errors.test.js +6 -1
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +1 -0
  71. package/dist/types.d.ts +5 -0
  72. package/package.json +1 -1
@@ -0,0 +1,15 @@
1
+ declare const _default: import("citty").CommandDef<{
2
+ readonly lines: {
3
+ readonly type: "string";
4
+ readonly description: "Lines of history to show (default 50)";
5
+ };
6
+ readonly follow: {
7
+ readonly type: "boolean";
8
+ readonly description: "Stream the log until interrupted";
9
+ };
10
+ readonly name: {
11
+ readonly type: "string";
12
+ readonly description: "Box name when running more than one (default: the single default box)";
13
+ };
14
+ }>;
15
+ export default _default;
@@ -0,0 +1,43 @@
1
+ import { spawn } from "node:child_process";
2
+ import { defineCommand } from "citty";
3
+ import { formatError, printResult } from "../../../output.js";
4
+ import { agentLog, agentLogFollowArgs } from "./box.js";
5
+ import { TestboxError } from "./errors.js";
6
+ import { nameArg, toEnvelope } from "./result.js";
7
+ function parseLines(raw) {
8
+ const lines = raw === undefined ? 50 : Number(raw);
9
+ if (!Number.isInteger(lines) || lines < 1)
10
+ throw new TestboxError("--lines must be a positive integer");
11
+ return lines;
12
+ }
13
+ export default defineCommand({
14
+ meta: { name: "logs", description: "The device agent's log inside the box" },
15
+ args: {
16
+ ...nameArg,
17
+ lines: { type: "string", description: "Lines of history to show (default 50)" },
18
+ follow: { type: "boolean", description: "Stream the log until interrupted" },
19
+ },
20
+ async run({ args }) {
21
+ if (!args.follow) {
22
+ printResult(await toEnvelope(() => agentLog(args.name, parseLines(args.lines))));
23
+ return;
24
+ }
25
+ const result = await toEnvelope(async () => agentLogFollowArgs(args.name, parseLines(args.lines)));
26
+ if (!result.ok) {
27
+ printResult(result);
28
+ return;
29
+ }
30
+ const child = spawn("docker", result.data, { stdio: "inherit" });
31
+ await new Promise((resolve) => {
32
+ child.on("error", (err) => {
33
+ printResult(formatError("api_error", err.message, true));
34
+ resolve();
35
+ });
36
+ child.on("close", (code) => {
37
+ if (code !== 0)
38
+ process.exitCode = 1;
39
+ resolve();
40
+ });
41
+ });
42
+ },
43
+ });
@@ -0,0 +1,11 @@
1
+ declare const _default: import("citty").CommandDef<{
2
+ readonly "keep-image": {
3
+ readonly type: "boolean";
4
+ readonly description: "Leave the image in the local Docker cache";
5
+ };
6
+ readonly name: {
7
+ readonly type: "string";
8
+ readonly description: "Box name when running more than one (default: the single default box)";
9
+ };
10
+ }>;
11
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { defineCommand } from "citty";
2
+ import { printResult } from "../../../output.js";
3
+ import { pruneTestbox } from "./box.js";
4
+ import { nameArg, toEnvelope } from "./result.js";
5
+ export default defineCommand({
6
+ meta: {
7
+ name: "prune",
8
+ description: "down, then remove everything: the device registration on the server, the profile, the local token and the image",
9
+ },
10
+ args: {
11
+ ...nameArg,
12
+ "keep-image": { type: "boolean", description: "Leave the image in the local Docker cache" },
13
+ },
14
+ async run({ args }) {
15
+ printResult(await toEnvelope(() => pruneTestbox(args.name, { keepImage: Boolean(args["keep-image"]) })));
16
+ },
17
+ });
@@ -0,0 +1,9 @@
1
+ import { type ClientDeps } from "../../../client.js";
2
+ import type { UserDevice } from "../../../types.js";
3
+ import { type DeviceConfig } from "../device-config.js";
4
+ export declare function readRegistration(name: string): DeviceConfig | null;
5
+ export declare function ensureRegistration(name: string, own: UserDevice[], deps: ClientDeps & {
6
+ hostName?: () => string;
7
+ }): Promise<DeviceConfig>;
8
+ /** Deletes the device row; false when it was already gone. */
9
+ export declare function unregister(deviceId: string, deps: ClientDeps): Promise<boolean>;
@@ -0,0 +1,56 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { hostname } from "node:os";
3
+ import { getBaseUrl, request } from "../../../client.js";
4
+ import { readDeviceConfig, writeDeviceConfig } from "../device-config.js";
5
+ import { getTestboxConfigPath } from "../paths.js";
6
+ import { DEFAULT_BOX_NAME } from "./container.js";
7
+ import { TestboxError } from "./errors.js";
8
+ export function readRegistration(name) {
9
+ return readDeviceConfig(getTestboxConfigPath(name));
10
+ }
11
+ function deviceNameFor(name, host) {
12
+ const machine = host.replace(/\.local$/, "");
13
+ const base = name === DEFAULT_BOX_NAME ? `${machine}-testbox` : `${machine}-testbox-${name}`;
14
+ return `${base}-${randomBytes(2).toString("hex")}`;
15
+ }
16
+ // One device row per box. The owner-scoped list can miss a row that still
17
+ // exists (another API key owner, another server), so those cases stop instead
18
+ // of quietly registering a second device.
19
+ export async function ensureRegistration(name, own, deps) {
20
+ const baseUrl = deps.baseUrl ?? getBaseUrl();
21
+ const existing = readRegistration(name);
22
+ if (existing) {
23
+ if (existing.baseUrl !== undefined && existing.baseUrl !== baseUrl) {
24
+ throw new TestboxError(`This testbox is registered against ${existing.baseUrl}, not ${baseUrl}. Run 'aximo device testbox prune' there first, or use a different --name.`);
25
+ }
26
+ if (own.some((d) => d.id === existing.deviceId))
27
+ return existing;
28
+ const org = await request("/api/organizations/devices", {}, deps);
29
+ if ((org.devices ?? []).some((d) => d.id === existing.deviceId)) {
30
+ throw new TestboxError(`Device ${existing.deviceId} belongs to another API key owner in this organization. Use that key, or 'aximo device testbox prune' with it.`);
31
+ }
32
+ }
33
+ const deviceName = existing?.name ?? deviceNameFor(name, (deps.hostName ?? hostname)());
34
+ const res = await request("/api/devices/register", { method: "POST", body: { name: deviceName, platform: "linux", os: "Testbox (Docker)" } }, deps);
35
+ const registration = {
36
+ deviceId: res.deviceId,
37
+ deviceToken: res.deviceToken,
38
+ name: deviceName,
39
+ registeredAt: new Date().toISOString(),
40
+ baseUrl,
41
+ };
42
+ writeDeviceConfig(registration, getTestboxConfigPath(name));
43
+ return registration;
44
+ }
45
+ /** Deletes the device row; false when it was already gone. */
46
+ export async function unregister(deviceId, deps) {
47
+ try {
48
+ await request(`/api/devices/user/${encodeURIComponent(deviceId)}`, { method: "DELETE" }, deps);
49
+ return true;
50
+ }
51
+ catch (err) {
52
+ if (err.status === 404)
53
+ return false;
54
+ throw err;
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ declare const _default: import("citty").CommandDef<{
2
+ readonly name: {
3
+ readonly type: "string";
4
+ readonly description: "Box name when running more than one (default: the single default box)";
5
+ };
6
+ }>;
7
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { defineCommand } from "citty";
2
+ import { printResult } from "../../../output.js";
3
+ import { resetTestbox } from "./box.js";
4
+ import { nameArg, toEnvelope } from "./result.js";
5
+ export default defineCommand({
6
+ meta: {
7
+ name: "reset",
8
+ description: "down, then drop the browser profile so the next sign-in starts fresh",
9
+ },
10
+ args: { ...nameArg },
11
+ async run({ args }) {
12
+ printResult(await toEnvelope(() => resetTestbox(args.name)));
13
+ },
14
+ });
@@ -0,0 +1,8 @@
1
+ import type { Envelope } from "../../../types.js";
2
+ export declare function toEnvelope<T>(fn: () => Promise<T>): Promise<Envelope<T>>;
3
+ export declare const nameArg: {
4
+ readonly name: {
5
+ readonly type: "string";
6
+ readonly description: "Box name when running more than one (default: the single default box)";
7
+ };
8
+ };
@@ -0,0 +1,23 @@
1
+ import { handleApiError } from "../../../errors.js";
2
+ import { formatError, formatSuccess } from "../../../output.js";
3
+ import { DockerCommandError } from "./docker.js";
4
+ import { TestboxError } from "./errors.js";
5
+ export async function toEnvelope(fn) {
6
+ try {
7
+ return formatSuccess(await fn());
8
+ }
9
+ catch (err) {
10
+ if (err instanceof TestboxError) {
11
+ return formatError(err.retryable ? "api_error" : "validation_error", err.message, err.retryable);
12
+ }
13
+ if (err instanceof DockerCommandError)
14
+ return formatError("api_error", err.message, true);
15
+ return handleApiError(err);
16
+ }
17
+ }
18
+ export const nameArg = {
19
+ name: {
20
+ type: "string",
21
+ description: "Box name when running more than one (default: the single default box)",
22
+ },
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DockerCommandError } from "./docker.js";
3
+ import { TestboxError } from "./errors.js";
4
+ import { toEnvelope } from "./result.js";
5
+ describe("toEnvelope", () => {
6
+ it("wraps a result in the success envelope", async () => {
7
+ expect(await toEnvelope(async () => ({ name: "default" }))).toEqual({
8
+ ok: true,
9
+ data: { name: "default" },
10
+ });
11
+ });
12
+ it("maps testbox, Docker and API failures to the CLI's error codes", async () => {
13
+ expect(await toEnvelope(async () => {
14
+ throw new TestboxError("Invalid testbox name");
15
+ })).toMatchObject({ ok: false, error: { code: "validation_error", retryable: false } });
16
+ expect(await toEnvelope(async () => {
17
+ throw new TestboxError("Could not pull", true);
18
+ })).toEqual({
19
+ ok: false,
20
+ error: { code: "api_error", message: "Could not pull", retryable: true },
21
+ });
22
+ expect(await toEnvelope(async () => {
23
+ throw new DockerCommandError(["rm", "-f", "x"], {
24
+ code: 1,
25
+ stdout: "",
26
+ stderr: "busy",
27
+ timedOut: false,
28
+ });
29
+ })).toMatchObject({ ok: false, error: { code: "api_error", retryable: true } });
30
+ expect(await toEnvelope(async () => {
31
+ throw Object.assign(new Error("Unauthorized"), { status: 401 });
32
+ })).toMatchObject({ ok: false, error: { code: "unauthorized" } });
33
+ });
34
+ });
@@ -0,0 +1,12 @@
1
+ export interface SelfCommand {
2
+ file: string;
3
+ prefix: string[];
4
+ }
5
+ export declare function selfCommand(env?: {
6
+ execPath: string;
7
+ execArgv: string[];
8
+ argv: string[];
9
+ sea: boolean;
10
+ }): SelfCommand;
11
+ export type DetachedSpawner = (args: string[], logPath: string, self?: SelfCommand) => number;
12
+ export declare const spawnDetached: DetachedSpawner;
@@ -0,0 +1,36 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, mkdirSync, openSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { isSea } from "node:sea";
5
+ // `aximo` is rarely on PATH where the MCP server runs (`npx … mcp`), and in the
6
+ // single-executable build argv[1] is the executable itself, not a script.
7
+ export function selfCommand(env = {
8
+ execPath: process.execPath,
9
+ execArgv: process.execArgv,
10
+ argv: process.argv,
11
+ sea: isSea(),
12
+ }) {
13
+ if (env.sea)
14
+ return { file: env.execPath, prefix: [] };
15
+ return { file: env.execPath, prefix: [...env.execArgv, env.argv[1] ?? ""] };
16
+ }
17
+ // The child outlives the MCP server (a Docker pull dies with its client) and
18
+ // must never touch the server's stdio, which carries JSON-RPC.
19
+ export const spawnDetached = (args, logPath, self = selfCommand()) => {
20
+ mkdirSync(dirname(logPath), { recursive: true });
21
+ // The log carries the up envelope, live-view password included.
22
+ const fd = openSync(logPath, "a", 0o600);
23
+ try {
24
+ const child = spawn(self.file, [...self.prefix, ...args], {
25
+ detached: true,
26
+ stdio: ["ignore", fd, fd],
27
+ windowsHide: true,
28
+ env: process.env,
29
+ });
30
+ child.unref();
31
+ return child.pid ?? -1;
32
+ }
33
+ finally {
34
+ closeSync(fd);
35
+ }
36
+ };
@@ -0,0 +1,58 @@
1
+ import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { selfCommand, spawnDetached } from "./self-command.js";
6
+ describe("selfCommand", () => {
7
+ it("re-runs the entry script through the same node, keeping loader flags", () => {
8
+ expect(selfCommand({
9
+ execPath: "/usr/bin/node",
10
+ execArgv: ["--import", "tsx"],
11
+ argv: ["/usr/bin/node", "/repo/dist/cli.js", "mcp"],
12
+ sea: false,
13
+ })).toEqual({ file: "/usr/bin/node", prefix: ["--import", "tsx", "/repo/dist/cli.js"] });
14
+ });
15
+ it("runs the single executable directly, where argv[1] is not a script", () => {
16
+ expect(selfCommand({
17
+ execPath: "C:\\aximo\\aximo.exe",
18
+ execArgv: [],
19
+ argv: ["C:\\aximo\\aximo.exe", "C:\\aximo\\aximo.exe", "mcp"],
20
+ sea: true,
21
+ })).toEqual({ file: "C:\\aximo\\aximo.exe", prefix: [] });
22
+ });
23
+ it("reads the live process by default", () => {
24
+ const cmd = selfCommand();
25
+ expect(cmd.file).toBe(process.execPath);
26
+ expect(cmd.prefix.at(-1)).toBe(process.argv[1]);
27
+ });
28
+ });
29
+ describe("spawnDetached", () => {
30
+ it("runs the command in the background with the log file as its only output", async () => {
31
+ const dir = mkdtempSync(join(tmpdir(), "aximo-spawn-"));
32
+ const logPath = join(dir, "nested", "box.log");
33
+ try {
34
+ const pid = spawnDetached(["device", "testbox", "up"], logPath, {
35
+ file: process.execPath,
36
+ prefix: ["-e", "console.log(process.argv.slice(1).join(' ')); console.error('done')"],
37
+ });
38
+ expect(pid).toBeGreaterThan(0);
39
+ let log = "";
40
+ for (let i = 0; i < 50 && !log.includes("done"); i++) {
41
+ await new Promise((r) => setTimeout(r, 100));
42
+ try {
43
+ log = readFileSync(logPath, "utf-8");
44
+ }
45
+ catch {
46
+ // not written yet
47
+ }
48
+ }
49
+ expect(log).toContain("device testbox up");
50
+ expect(log).toContain("done");
51
+ if (process.platform !== "win32")
52
+ expect(statSync(logPath).mode & 0o777).toBe(0o600);
53
+ }
54
+ finally {
55
+ rmSync(dir, { recursive: true, force: true });
56
+ }
57
+ });
58
+ });
@@ -0,0 +1,7 @@
1
+ declare const _default: import("citty").CommandDef<{
2
+ readonly name: {
3
+ readonly type: "string";
4
+ readonly description: "Box name when running more than one (default: the single default box)";
5
+ };
6
+ }>;
7
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { defineCommand } from "citty";
2
+ import { printResult } from "../../../output.js";
3
+ import { statusTestbox } from "./box.js";
4
+ import { nameArg, toEnvelope } from "./result.js";
5
+ export default defineCommand({
6
+ meta: { name: "status", description: "Container, agent and device state of the box" },
7
+ args: { ...nameArg },
8
+ async run({ args }) {
9
+ printResult(await toEnvelope(() => statusTestbox(args.name)));
10
+ },
11
+ });
@@ -0,0 +1,47 @@
1
+ import { type UpOptions } from "./box.js";
2
+ export declare function upOptionsFromArgs(args: {
3
+ name?: string | undefined;
4
+ ports?: string | undefined;
5
+ "block-hosts"?: string | undefined;
6
+ "start-url"?: string | undefined;
7
+ "novnc-port"?: string | undefined;
8
+ screen?: string | undefined;
9
+ image?: string | undefined;
10
+ wait?: boolean | undefined;
11
+ }): UpOptions;
12
+ declare const _default: import("citty").CommandDef<{
13
+ readonly ports: {
14
+ readonly type: "string";
15
+ readonly description: "Host ports to mirror into the box, e.g. 3000 or 3000,5173-5180 (default 3000)";
16
+ };
17
+ readonly "block-hosts": {
18
+ readonly type: "string";
19
+ readonly description: "Comma-separated hosts the browser must refuse, e.g. your production domain (a browser rule matched by name, not a network boundary)";
20
+ };
21
+ readonly "start-url": {
22
+ readonly type: "string";
23
+ readonly description: "Page the browser opens on start";
24
+ };
25
+ readonly "novnc-port": {
26
+ readonly type: "string";
27
+ readonly description: "Host port for the live view (default 6080, next free for extra boxes)";
28
+ };
29
+ readonly screen: {
30
+ readonly type: "string";
31
+ readonly description: "Virtual screen size WIDTHxHEIGHT (default 1280x720, the same as Aximo's cloud desktop)";
32
+ };
33
+ readonly image: {
34
+ readonly type: "string";
35
+ readonly description: "Image to run instead of the published one";
36
+ };
37
+ readonly wait: {
38
+ readonly type: "boolean";
39
+ readonly default: true;
40
+ readonly description: "Wait until the device reports online (up to 2 minutes)";
41
+ };
42
+ readonly name: {
43
+ readonly type: "string";
44
+ readonly description: "Box name when running more than one (default: the single default box)";
45
+ };
46
+ }>;
47
+ export default _default;
@@ -0,0 +1,76 @@
1
+ import { defineCommand } from "citty";
2
+ import { printResult } from "../../../output.js";
3
+ import { upTestbox } from "./box.js";
4
+ import { nameArg, toEnvelope } from "./result.js";
5
+ function envDefault(flag, env) {
6
+ return flag ?? process.env[env];
7
+ }
8
+ export function upOptionsFromArgs(args) {
9
+ const options = { wait: args.wait !== false };
10
+ if (args.name !== undefined)
11
+ options.name = args.name;
12
+ const ports = envDefault(args.ports, "AXIMO_TESTBOX_PORTS");
13
+ if (ports !== undefined)
14
+ options.ports = ports;
15
+ const blockHosts = envDefault(args["block-hosts"], "AXIMO_TESTBOX_BLOCK_HOSTS");
16
+ if (blockHosts !== undefined) {
17
+ options.blockHosts = blockHosts
18
+ .split(",")
19
+ .map((h) => h.trim())
20
+ .filter(Boolean);
21
+ }
22
+ const startUrl = envDefault(args["start-url"], "AXIMO_TESTBOX_START_URL");
23
+ if (startUrl !== undefined)
24
+ options.startUrl = startUrl;
25
+ const novnc = envDefault(args["novnc-port"], "AXIMO_TESTBOX_NOVNC_PORT");
26
+ if (novnc !== undefined)
27
+ options.novncPort = Number(novnc);
28
+ const screen = envDefault(args.screen, "AXIMO_TESTBOX_SCREEN");
29
+ if (screen !== undefined)
30
+ options.screen = screen;
31
+ if (args.image !== undefined)
32
+ options.image = args.image;
33
+ return options;
34
+ }
35
+ export default defineCommand({
36
+ meta: {
37
+ name: "up",
38
+ description: "Pull the image, register the box as a local device, start it and connect it. Safe to re-run; a setting comes from the flag, else the same-named AXIMO_TESTBOX_* variable, else the running box.",
39
+ },
40
+ args: {
41
+ ...nameArg,
42
+ ports: {
43
+ type: "string",
44
+ description: "Host ports to mirror into the box, e.g. 3000 or 3000,5173-5180 (default 3000)",
45
+ },
46
+ "block-hosts": {
47
+ type: "string",
48
+ description: "Comma-separated hosts the browser must refuse, e.g. your production domain (a browser rule matched by name, not a network boundary)",
49
+ },
50
+ "start-url": { type: "string", description: "Page the browser opens on start" },
51
+ "novnc-port": {
52
+ type: "string",
53
+ description: "Host port for the live view (default 6080, next free for extra boxes)",
54
+ },
55
+ screen: {
56
+ type: "string",
57
+ description: "Virtual screen size WIDTHxHEIGHT (default 1280x720, the same as Aximo's cloud desktop)",
58
+ },
59
+ image: { type: "string", description: "Image to run instead of the published one" },
60
+ wait: {
61
+ type: "boolean",
62
+ default: true,
63
+ description: "Wait until the device reports online (up to 2 minutes)",
64
+ },
65
+ },
66
+ async run({ args }) {
67
+ const result = await toEnvelope(() => upTestbox(upOptionsFromArgs(args)));
68
+ printResult(result);
69
+ if (result.ok) {
70
+ for (const note of result.data.notes)
71
+ console.error(`\nNote: ${note}`);
72
+ if (result.data.liveViewUrl)
73
+ console.error(`\nLive view: ${result.data.liveViewUrl}`);
74
+ }
75
+ },
76
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { upOptionsFromArgs } from "./up.js";
3
+ const ENV = [
4
+ "AXIMO_TESTBOX_PORTS",
5
+ "AXIMO_TESTBOX_BLOCK_HOSTS",
6
+ "AXIMO_TESTBOX_START_URL",
7
+ "AXIMO_TESTBOX_NOVNC_PORT",
8
+ "AXIMO_TESTBOX_SCREEN",
9
+ ];
10
+ afterEach(() => {
11
+ for (const key of ENV)
12
+ delete process.env[key];
13
+ });
14
+ describe("upOptionsFromArgs", () => {
15
+ it("passes only the flags that were given, with wait on by default", () => {
16
+ expect(upOptionsFromArgs({})).toEqual({ wait: true });
17
+ expect(upOptionsFromArgs({ name: "qa", ports: "5173", image: "x:1", wait: false })).toEqual({
18
+ wait: false,
19
+ name: "qa",
20
+ ports: "5173",
21
+ image: "x:1",
22
+ });
23
+ });
24
+ it("splits block hosts and parses the noVNC port", () => {
25
+ expect(upOptionsFromArgs({
26
+ "block-hosts": "a.com, b.com,",
27
+ "novnc-port": "6090",
28
+ "start-url": "http://x",
29
+ screen: "1920x1080",
30
+ })).toMatchObject({
31
+ blockHosts: ["a.com", "b.com"],
32
+ novncPort: 6090,
33
+ startUrl: "http://x",
34
+ screen: "1920x1080",
35
+ });
36
+ });
37
+ it("falls back to the AXIMO_TESTBOX_* variables the Power's script used", () => {
38
+ process.env["AXIMO_TESTBOX_PORTS"] = "8080";
39
+ process.env["AXIMO_TESTBOX_BLOCK_HOSTS"] = "prod.example.com";
40
+ process.env["AXIMO_TESTBOX_START_URL"] = "http://localhost:8080/";
41
+ process.env["AXIMO_TESTBOX_NOVNC_PORT"] = "6081";
42
+ process.env["AXIMO_TESTBOX_SCREEN"] = "1600x900";
43
+ expect(upOptionsFromArgs({ ports: "3000" })).toEqual({
44
+ wait: true,
45
+ ports: "3000",
46
+ blockHosts: ["prod.example.com"],
47
+ startUrl: "http://localhost:8080/",
48
+ novncPort: 6081,
49
+ screen: "1600x900",
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,7 @@
1
+ declare const _default: import("citty").CommandDef<{
2
+ readonly name: {
3
+ readonly type: "string";
4
+ readonly description: "Box name when running more than one (default: the single default box)";
5
+ };
6
+ }>;
7
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import { defineCommand } from "citty";
2
+ import { printResult } from "../../../output.js";
3
+ import { statusTestbox } from "./box.js";
4
+ import { TestboxError } from "./errors.js";
5
+ import { nameArg, toEnvelope } from "./result.js";
6
+ export default defineCommand({
7
+ meta: { name: "url", description: "The live-view (noVNC) link, password included" },
8
+ args: { ...nameArg },
9
+ async run({ args }) {
10
+ printResult(await toEnvelope(async () => {
11
+ const status = await statusTestbox(args.name);
12
+ if (!status.liveViewUrl) {
13
+ throw new TestboxError(status.container === "running"
14
+ ? "The live view is not ready yet; try again in a moment"
15
+ : "The box is not running; start it with: aximo device testbox up");
16
+ }
17
+ return { liveViewUrl: status.liveViewUrl };
18
+ }));
19
+ },
20
+ });
@@ -0,0 +1,4 @@
1
+ import { type ClientDeps } from "../../client.js";
2
+ import type { UserDevice } from "../../types.js";
3
+ /** The BYOD devices registered by this API key's owner. */
4
+ export declare function listUserDevices(deps?: ClientDeps): Promise<UserDevice[]>;
@@ -0,0 +1,18 @@
1
+ import { request } from "../../client.js";
2
+ /** The BYOD devices registered by this API key's owner. */
3
+ export async function listUserDevices(deps = {}) {
4
+ try {
5
+ const res = await request("/api/devices/user", {}, deps);
6
+ return res.devices ?? [];
7
+ }
8
+ catch (err) {
9
+ // The route answers a bare 404 when the org has no BYOD feature.
10
+ if (err.status === 404) {
11
+ throw Object.assign(new Error("BYOD is not enabled for this organization"), {
12
+ status: 403,
13
+ details: { feature: "byodEnabled" },
14
+ });
15
+ }
16
+ throw err;
17
+ }
18
+ }
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { ClientDeps } from "../../client.js";
3
- export declare function createMcpServer(deps?: ClientDeps): McpServer;
2
+ import { type ToolDeps } from "./tools.js";
3
+ export declare function createMcpServer(deps?: ToolDeps): McpServer;