@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,160 @@
1
+ import { defineCommand } from "citty";
2
+ import { closeSync, existsSync, openSync, readSync, statSync, truncateSync, unwatchFile, watchFile, } from "node:fs";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ import { formatError, formatSuccess, printResult } from "../../output.js";
5
+ import { getRemoteLogPath } from "./paths.js";
6
+ const DEFAULT_LINES = 50;
7
+ const MAX_TAIL_BYTES = 1_024 * 1_024; // 1MB — enough for many hundreds of log lines
8
+ function readTail(logPath, lines) {
9
+ const size = statSync(logPath).size;
10
+ if (size === 0)
11
+ return { lines: [], truncated: false };
12
+ const bytesToRead = Math.min(size, MAX_TAIL_BYTES);
13
+ const start = size - bytesToRead;
14
+ const buffer = Buffer.alloc(bytesToRead);
15
+ const fd = openSync(logPath, "r");
16
+ try {
17
+ readSync(fd, buffer, 0, bytesToRead, start);
18
+ }
19
+ finally {
20
+ closeSync(fd);
21
+ }
22
+ const text = buffer.toString("utf-8");
23
+ const all = text.split(/\r?\n/);
24
+ if (start > 0 && all.length > 0)
25
+ all.shift(); // drop partial first line
26
+ while (all.length > 0 && all[all.length - 1] === "")
27
+ all.pop();
28
+ const tail = all.slice(-lines);
29
+ return { lines: tail, truncated: start > 0 || all.length > lines };
30
+ }
31
+ export function runDeviceLogs(options = {}) {
32
+ const path = getRemoteLogPath();
33
+ if (options.path) {
34
+ return formatSuccess({ path });
35
+ }
36
+ if (options.clear) {
37
+ if (!existsSync(path)) {
38
+ return formatSuccess({ path, cleared: true });
39
+ }
40
+ try {
41
+ truncateSync(path, 0);
42
+ return formatSuccess({ path, cleared: true });
43
+ }
44
+ catch (err) {
45
+ return formatError("api_error", `Failed to clear log: ${err instanceof Error ? err.message : String(err)}`, false);
46
+ }
47
+ }
48
+ if (!existsSync(path)) {
49
+ return formatError("not_found", `Log file does not exist yet: ${path}`, false, "Start the agent with: aximo device connect");
50
+ }
51
+ const count = options.lines ?? DEFAULT_LINES;
52
+ if (!Number.isInteger(count) || count <= 0) {
53
+ return formatError("validation_error", "--lines must be a positive integer", false);
54
+ }
55
+ try {
56
+ const { lines, truncated } = readTail(path, count);
57
+ return formatSuccess({ path, lines, truncated });
58
+ }
59
+ catch (err) {
60
+ return formatError("api_error", `Failed to read log: ${err instanceof Error ? err.message : String(err)}`, false);
61
+ }
62
+ }
63
+ async function followLog() {
64
+ const path = getRemoteLogPath();
65
+ if (!existsSync(path)) {
66
+ console.error(`Waiting for log file: ${path}`);
67
+ }
68
+ let lastSize = existsSync(path) ? statSync(path).size : 0;
69
+ const decoder = new StringDecoder("utf8");
70
+ let carry = "";
71
+ const emit = (chunk) => {
72
+ if (!chunk)
73
+ return;
74
+ const combined = carry + chunk;
75
+ const lastNewline = combined.lastIndexOf("\n");
76
+ if (lastNewline === -1) {
77
+ carry = combined;
78
+ return;
79
+ }
80
+ const complete = combined.slice(0, lastNewline + 1);
81
+ carry = combined.slice(lastNewline + 1);
82
+ process.stdout.write(complete);
83
+ };
84
+ if (lastSize > 0) {
85
+ const { lines } = readTail(path, DEFAULT_LINES);
86
+ if (lines.length > 0)
87
+ process.stdout.write(`${lines.join("\n")}\n`);
88
+ }
89
+ return new Promise((_, reject) => {
90
+ const onChange = (curr) => {
91
+ if (curr.size < lastSize) {
92
+ lastSize = 0;
93
+ carry = "";
94
+ decoder.end();
95
+ }
96
+ if (curr.size <= lastSize)
97
+ return;
98
+ const toRead = curr.size - lastSize;
99
+ const buffer = Buffer.alloc(toRead);
100
+ let fd = null;
101
+ try {
102
+ fd = openSync(path, "r");
103
+ readSync(fd, buffer, 0, toRead, lastSize);
104
+ lastSize = curr.size;
105
+ emit(decoder.write(buffer));
106
+ }
107
+ catch (err) {
108
+ unwatchFile(path);
109
+ reject(err);
110
+ }
111
+ finally {
112
+ if (fd !== null)
113
+ closeSync(fd);
114
+ }
115
+ };
116
+ watchFile(path, { interval: 500 }, onChange);
117
+ const cleanup = () => {
118
+ const tail = decoder.end() + carry;
119
+ if (tail)
120
+ process.stdout.write(tail);
121
+ unwatchFile(path);
122
+ process.exit(0);
123
+ };
124
+ process.on("SIGINT", cleanup);
125
+ process.on("SIGTERM", cleanup);
126
+ });
127
+ }
128
+ export default defineCommand({
129
+ meta: {
130
+ name: "logs",
131
+ description: "Show the BYOD remote agent log",
132
+ },
133
+ args: {
134
+ follow: {
135
+ type: "boolean",
136
+ alias: "f",
137
+ description: "Stream new log lines as they are written",
138
+ },
139
+ lines: {
140
+ type: "string",
141
+ alias: "n",
142
+ description: `Number of lines to show (default ${DEFAULT_LINES})`,
143
+ },
144
+ path: { type: "boolean", description: "Print log file path and exit" },
145
+ clear: { type: "boolean", description: "Truncate the log file" },
146
+ },
147
+ async run({ args }) {
148
+ if (args.follow) {
149
+ await followLog();
150
+ return;
151
+ }
152
+ const options = {
153
+ path: Boolean(args.path),
154
+ clear: Boolean(args.clear),
155
+ };
156
+ if (args.lines !== undefined)
157
+ options.lines = Number(args.lines);
158
+ printResult(runDeviceLogs(options));
159
+ },
160
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { runDeviceLogs } from "./logs.js";
6
+ let tempHome;
7
+ beforeAll(() => {
8
+ tempHome = mkdtempSync(join(tmpdir(), "aximo-logs-"));
9
+ process.env["AXIMO_HOME"] = tempHome;
10
+ });
11
+ afterAll(() => {
12
+ delete process.env["AXIMO_HOME"];
13
+ rmSync(tempHome, { recursive: true, force: true });
14
+ });
15
+ beforeEach(() => {
16
+ rmSync(tempHome, { recursive: true, force: true });
17
+ mkdirSync(tempHome, { recursive: true });
18
+ });
19
+ afterEach(() => {
20
+ rmSync(tempHome, { recursive: true, force: true });
21
+ });
22
+ describe("runDeviceLogs", () => {
23
+ it("--path returns the log file path without requiring the file to exist", () => {
24
+ const result = runDeviceLogs({ path: true });
25
+ expect(result.ok).toBe(true);
26
+ if (result.ok)
27
+ expect(result.data.path).toBe(join(tempHome, "remote.log"));
28
+ });
29
+ it("returns not_found when the log doesn't exist", () => {
30
+ const result = runDeviceLogs();
31
+ expect(result.ok).toBe(false);
32
+ if (!result.ok)
33
+ expect(result.error.code).toBe("not_found");
34
+ });
35
+ it("returns last N lines of the log", () => {
36
+ const logPath = join(tempHome, "remote.log");
37
+ writeFileSync(logPath, "a\nb\nc\nd\ne\n");
38
+ const result = runDeviceLogs({ lines: 3 });
39
+ expect(result.ok).toBe(true);
40
+ if (result.ok) {
41
+ expect(result.data.lines).toEqual(["c", "d", "e"]);
42
+ expect(result.data.truncated).toBe(true);
43
+ }
44
+ });
45
+ it("marks truncated=false when all lines fit", () => {
46
+ const logPath = join(tempHome, "remote.log");
47
+ writeFileSync(logPath, "a\nb\nc\n");
48
+ const result = runDeviceLogs({ lines: 5 });
49
+ expect(result.ok).toBe(true);
50
+ if (result.ok) {
51
+ expect(result.data.lines).toEqual(["a", "b", "c"]);
52
+ expect(result.data.truncated).toBe(false);
53
+ }
54
+ });
55
+ it("--clear truncates the log", () => {
56
+ const logPath = join(tempHome, "remote.log");
57
+ writeFileSync(logPath, "content");
58
+ const result = runDeviceLogs({ clear: true });
59
+ expect(result.ok).toBe(true);
60
+ expect(existsSync(logPath)).toBe(true);
61
+ expect(readFileSync(logPath, "utf-8")).toBe("");
62
+ });
63
+ it("rejects invalid --lines with validation_error", () => {
64
+ writeFileSync(join(tempHome, "remote.log"), "x\n");
65
+ const result = runDeviceLogs({ lines: 0 });
66
+ expect(result.ok).toBe(false);
67
+ if (!result.ok)
68
+ expect(result.error.code).toBe("validation_error");
69
+ });
70
+ });
@@ -0,0 +1,12 @@
1
+ export declare function getAximoDir(): string;
2
+ export declare function getDeviceConfigPath(): string;
3
+ export declare function getPythonDir(): string;
4
+ export declare function getPythonMarkerPath(): string;
5
+ export declare function getDepsMarkerPath(): string;
6
+ export declare function getCacheDir(): string;
7
+ export declare function getRemoteLogPath(): string;
8
+ export declare function getAgentDir(): string;
9
+ export declare function getAgentScriptPath(): string;
10
+ export declare function getAgentRequirementsPath(): string;
11
+ export declare function getAgentManifestPath(): string;
12
+ export declare function getEmbeddedPythonBinary(): string;
@@ -0,0 +1,40 @@
1
+ import { homedir, platform } from "node:os";
2
+ import { join } from "node:path";
3
+ export function getAximoDir() {
4
+ return process.env["AXIMO_HOME"] ?? join(homedir(), ".aximo");
5
+ }
6
+ export function getDeviceConfigPath() {
7
+ return join(getAximoDir(), "device.json");
8
+ }
9
+ export function getPythonDir() {
10
+ return join(getAximoDir(), "python");
11
+ }
12
+ export function getPythonMarkerPath() {
13
+ return join(getAximoDir(), "python.version");
14
+ }
15
+ export function getDepsMarkerPath() {
16
+ return join(getAximoDir(), "python.deps.sha256");
17
+ }
18
+ export function getCacheDir() {
19
+ return join(getAximoDir(), "cache");
20
+ }
21
+ export function getRemoteLogPath() {
22
+ return join(getAximoDir(), "remote.log");
23
+ }
24
+ export function getAgentDir() {
25
+ return join(getAximoDir(), "agent");
26
+ }
27
+ export function getAgentScriptPath() {
28
+ return join(getAgentDir(), "remote.py");
29
+ }
30
+ export function getAgentRequirementsPath() {
31
+ return join(getAgentDir(), "requirements.txt");
32
+ }
33
+ export function getAgentManifestPath() {
34
+ return join(getAgentDir(), "manifest.json");
35
+ }
36
+ export function getEmbeddedPythonBinary() {
37
+ return platform() === "win32"
38
+ ? join(getPythonDir(), "python.exe")
39
+ : join(getPythonDir(), "bin", "python3");
40
+ }
@@ -0,0 +1,21 @@
1
+ import { type ClientDeps } from "../../client.js";
2
+ import type { Envelope } from "../../types.js";
3
+ export interface PingResult {
4
+ deviceId: string;
5
+ name: string;
6
+ status: "online" | "offline" | string;
7
+ lastSeenAt?: string;
8
+ lastSeenAgoSeconds?: number;
9
+ latencyMs: number;
10
+ }
11
+ export interface RunDevicePingOptions {
12
+ deviceId?: string;
13
+ }
14
+ export declare function runDevicePing(options?: RunDevicePingOptions, deps?: ClientDeps): Promise<Envelope<PingResult>>;
15
+ declare const _default: import("citty").CommandDef<{
16
+ readonly "device-id": {
17
+ readonly type: "string";
18
+ readonly description: "Device ID (defaults to local device)";
19
+ };
20
+ }>;
21
+ export default _default;
@@ -0,0 +1,66 @@
1
+ import { defineCommand } from "citty";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { request } from "../../client.js";
4
+ import { handleApiError } from "../../errors.js";
5
+ import { formatError, formatSuccess, printResult } from "../../output.js";
6
+ import { getDeviceConfigPath } from "./paths.js";
7
+ function resolveDeviceId(explicit) {
8
+ if (explicit)
9
+ return explicit;
10
+ const configPath = getDeviceConfigPath();
11
+ if (!existsSync(configPath))
12
+ return null;
13
+ try {
14
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
15
+ return typeof config.deviceId === "string" ? config.deviceId : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ export async function runDevicePing(options = {}, deps) {
22
+ const deviceId = resolveDeviceId(options.deviceId);
23
+ if (!deviceId) {
24
+ return formatError("validation_error", "No device ID provided and no local device config found", false, 'Register first: aximo device register --name "My PC", or pass --device-id');
25
+ }
26
+ const start = Date.now();
27
+ try {
28
+ const res = await request("/api/devices/user", {}, deps ?? {});
29
+ const latencyMs = Date.now() - start;
30
+ const device = res.devices?.find((d) => d.id === deviceId);
31
+ if (!device) {
32
+ return formatError("not_found", `Device ${deviceId} not visible for the current API key`, false, "Token may belong to another account, or the device was deleted server-side");
33
+ }
34
+ const lastSeenAgoSeconds = device.lastSeenAt
35
+ ? Math.max(0, Math.round((Date.now() - Date.parse(device.lastSeenAt)) / 1000))
36
+ : undefined;
37
+ return formatSuccess({
38
+ deviceId: device.id,
39
+ name: device.name,
40
+ status: device.status,
41
+ ...(device.lastSeenAt !== undefined ? { lastSeenAt: device.lastSeenAt } : {}),
42
+ ...(lastSeenAgoSeconds !== undefined ? { lastSeenAgoSeconds } : {}),
43
+ latencyMs,
44
+ });
45
+ }
46
+ catch (err) {
47
+ return handleApiError(err);
48
+ }
49
+ }
50
+ export default defineCommand({
51
+ meta: {
52
+ name: "ping",
53
+ description: "Check the current online status of your device from Aximo",
54
+ },
55
+ args: {
56
+ "device-id": { type: "string", description: "Device ID (defaults to local device)" },
57
+ },
58
+ async run({ args }) {
59
+ const deviceId = args["device-id"];
60
+ const result = await runDevicePing(deviceId ? { deviceId } : {});
61
+ printResult(result);
62
+ if (result.ok && result.data.status !== "online") {
63
+ process.exitCode = 1;
64
+ }
65
+ },
66
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
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 { runDevicePing } from "./ping.js";
7
+ let tempHome;
8
+ beforeAll(() => {
9
+ tempHome = mkdtempSync(join(tmpdir(), "aximo-ping-"));
10
+ process.env["AXIMO_HOME"] = tempHome;
11
+ process.env["AXIMO_API_KEY"] = "test-key";
12
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
13
+ server.listen();
14
+ });
15
+ afterAll(() => {
16
+ delete process.env["AXIMO_HOME"];
17
+ server.close();
18
+ rmSync(tempHome, { recursive: true, force: true });
19
+ });
20
+ beforeEach(() => {
21
+ rmSync(tempHome, { recursive: true, force: true });
22
+ mkdirSync(tempHome, { recursive: true });
23
+ });
24
+ afterEach(() => server.resetHandlers());
25
+ function seedDeviceConfig(deviceId) {
26
+ writeFileSync(join(tempHome, "device.json"), JSON.stringify({ deviceId, deviceToken: "t", name: "Test PC" }));
27
+ }
28
+ describe("runDevicePing", () => {
29
+ it("errors when no device config and no deviceId flag", async () => {
30
+ const result = await runDevicePing();
31
+ expect(result.ok).toBe(false);
32
+ if (!result.ok)
33
+ expect(result.error.code).toBe("validation_error");
34
+ });
35
+ it("reports online status with latency when device is found", async () => {
36
+ const deviceId = "550e8400-e29b-41d4-a716-446655440000";
37
+ seedDeviceConfig(deviceId);
38
+ const now = new Date().toISOString();
39
+ server.use(http.get(`${TEST_BASE_URL}/api/devices/user`, () => HttpResponse.json({
40
+ devices: [{ id: deviceId, name: "Test PC", status: "online", lastSeenAt: now }],
41
+ })));
42
+ const result = await runDevicePing();
43
+ expect(result.ok).toBe(true);
44
+ if (result.ok) {
45
+ expect(result.data.status).toBe("online");
46
+ expect(result.data.deviceId).toBe(deviceId);
47
+ expect(result.data.latencyMs).toBeGreaterThanOrEqual(0);
48
+ }
49
+ });
50
+ it("returns not_found when device isn't in the response", async () => {
51
+ seedDeviceConfig("550e8400-e29b-41d4-a716-446655440000");
52
+ server.use(http.get(`${TEST_BASE_URL}/api/devices/user`, () => HttpResponse.json({ devices: [] })));
53
+ const result = await runDevicePing();
54
+ expect(result.ok).toBe(false);
55
+ if (!result.ok)
56
+ expect(result.error.code).toBe("not_found");
57
+ });
58
+ it("accepts an explicit --device-id", async () => {
59
+ const deviceId = "aaaa-bbbb";
60
+ server.use(http.get(`${TEST_BASE_URL}/api/devices/user`, () => HttpResponse.json({
61
+ devices: [{ id: deviceId, name: "Other", status: "offline" }],
62
+ })));
63
+ const result = await runDevicePing({ deviceId });
64
+ expect(result.ok).toBe(true);
65
+ if (result.ok)
66
+ expect(result.data.status).toBe("offline");
67
+ });
68
+ });
@@ -0,0 +1,23 @@
1
+ import { type ClientDeps } from "../../client.js";
2
+ import type { Envelope } from "../../types.js";
3
+ export interface RegisterResult {
4
+ deviceId: string;
5
+ name: string;
6
+ registeredAt: string;
7
+ }
8
+ export declare function runDeviceRegister(options: {
9
+ name: string;
10
+ force?: boolean;
11
+ }, deps?: ClientDeps): Promise<Envelope<RegisterResult>>;
12
+ declare const _default: import("citty").CommandDef<{
13
+ readonly name: {
14
+ readonly type: "string";
15
+ readonly description: "Device name";
16
+ readonly required: true;
17
+ };
18
+ readonly force: {
19
+ readonly type: "boolean";
20
+ readonly description: "Overwrite an existing local registration (orphans the previous device)";
21
+ };
22
+ }>;
23
+ export default _default;
@@ -0,0 +1,83 @@
1
+ import { defineCommand } from "citty";
2
+ import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { platform, release } from "node:os";
4
+ import { request } from "../../client.js";
5
+ import { handleApiError } from "../../errors.js";
6
+ import { formatError, formatSuccess, printResult } from "../../output.js";
7
+ import { getAximoDir, getDeviceConfigPath } from "./paths.js";
8
+ function detectPlatform() {
9
+ const p = platform();
10
+ if (p === "win32")
11
+ return "windows";
12
+ if (p === "darwin")
13
+ return "macos";
14
+ return "linux";
15
+ }
16
+ function detectOs() {
17
+ const p = platform();
18
+ const r = release();
19
+ if (p === "win32")
20
+ return `Windows ${r}`;
21
+ if (p === "darwin")
22
+ return `macOS ${r}`;
23
+ return `Linux ${r}`;
24
+ }
25
+ export async function runDeviceRegister(options, deps) {
26
+ try {
27
+ const configPath = getDeviceConfigPath();
28
+ if (existsSync(configPath) && !options.force) {
29
+ return formatError("validation_error", "A device is already registered on this machine. Run 'aximo device unregister' first, or pass --force to overwrite (this orphans the previous server-side device).", false);
30
+ }
31
+ const res = await request("/api/devices/register", {
32
+ method: "POST",
33
+ body: {
34
+ name: options.name,
35
+ platform: detectPlatform(),
36
+ os: detectOs(),
37
+ },
38
+ }, deps ?? {});
39
+ const aximoDir = getAximoDir();
40
+ if (!existsSync(aximoDir)) {
41
+ mkdirSync(aximoDir, { recursive: true });
42
+ }
43
+ const registeredAt = new Date().toISOString();
44
+ const config = {
45
+ deviceId: res.deviceId,
46
+ deviceToken: res.deviceToken,
47
+ name: options.name,
48
+ registeredAt,
49
+ };
50
+ writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
51
+ try {
52
+ chmodSync(configPath, 0o600);
53
+ }
54
+ catch {
55
+ // chmod may fail on Windows, file-level ACLs apply instead
56
+ }
57
+ return formatSuccess({
58
+ deviceId: res.deviceId,
59
+ name: options.name,
60
+ registeredAt,
61
+ });
62
+ }
63
+ catch (err) {
64
+ return handleApiError(err);
65
+ }
66
+ }
67
+ export default defineCommand({
68
+ meta: { name: "register", description: "Register this machine as an Aximo device" },
69
+ args: {
70
+ name: {
71
+ type: "string",
72
+ description: "Device name",
73
+ required: true,
74
+ },
75
+ force: {
76
+ type: "boolean",
77
+ description: "Overwrite an existing local registration (orphans the previous device)",
78
+ },
79
+ },
80
+ async run({ args }) {
81
+ printResult(await runDeviceRegister({ name: args.name, force: Boolean(args.force) }));
82
+ },
83
+ });
@@ -0,0 +1,30 @@
1
+ import type { Envelope } from "../../types.js";
2
+ export interface ResetResult {
3
+ removed: string[];
4
+ }
5
+ export interface RunDeviceResetOptions {
6
+ python?: boolean;
7
+ deps?: boolean;
8
+ device?: boolean;
9
+ all?: boolean;
10
+ }
11
+ export declare function runDeviceReset(options?: RunDeviceResetOptions): Envelope<ResetResult>;
12
+ declare const _default: import("citty").CommandDef<{
13
+ readonly python: {
14
+ readonly type: "boolean";
15
+ readonly description: "Remove embedded Python and markers";
16
+ };
17
+ readonly deps: {
18
+ readonly type: "boolean";
19
+ readonly description: "Remove deps marker so the next setup reinstalls packages";
20
+ };
21
+ readonly device: {
22
+ readonly type: "boolean";
23
+ readonly description: "Remove local device credentials and agent log";
24
+ };
25
+ readonly all: {
26
+ readonly type: "boolean";
27
+ readonly description: "Remove the entire ~/.aximo directory";
28
+ };
29
+ }>;
30
+ export default _default;
@@ -0,0 +1,67 @@
1
+ import { defineCommand } from "citty";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { formatError, formatSuccess, printResult } from "../../output.js";
4
+ import { getAximoDir, getCacheDir, getDepsMarkerPath, getDeviceConfigPath, getPythonDir, getPythonMarkerPath, getRemoteLogPath, } from "./paths.js";
5
+ function removeIfExists(path, removed, opts = {}) {
6
+ if (!existsSync(path))
7
+ return;
8
+ rmSync(path, { recursive: opts.recursive ?? false, force: true });
9
+ removed.push(path);
10
+ }
11
+ export function runDeviceReset(options = {}) {
12
+ const { python, deps, device, all } = options;
13
+ if (!python && !deps && !device && !all) {
14
+ return formatError("validation_error", "Pass at least one of --python, --deps, --device, or --all", false);
15
+ }
16
+ const removed = [];
17
+ try {
18
+ if (all) {
19
+ removeIfExists(getAximoDir(), removed, { recursive: true });
20
+ return formatSuccess({ removed });
21
+ }
22
+ if (python) {
23
+ removeIfExists(getPythonDir(), removed, { recursive: true });
24
+ removeIfExists(getPythonMarkerPath(), removed);
25
+ removeIfExists(getDepsMarkerPath(), removed);
26
+ removeIfExists(getCacheDir(), removed, { recursive: true });
27
+ }
28
+ else if (deps) {
29
+ removeIfExists(getDepsMarkerPath(), removed);
30
+ }
31
+ if (device) {
32
+ removeIfExists(getDeviceConfigPath(), removed);
33
+ removeIfExists(getRemoteLogPath(), removed);
34
+ }
35
+ return formatSuccess({ removed });
36
+ }
37
+ catch (err) {
38
+ return formatError("api_error", `Reset failed: ${err instanceof Error ? err.message : String(err)}`, false);
39
+ }
40
+ }
41
+ export default defineCommand({
42
+ meta: {
43
+ name: "reset",
44
+ description: "Remove BYOD local state (Python install, deps, device credentials)",
45
+ },
46
+ args: {
47
+ python: { type: "boolean", description: "Remove embedded Python and markers" },
48
+ deps: {
49
+ type: "boolean",
50
+ description: "Remove deps marker so the next setup reinstalls packages",
51
+ },
52
+ device: { type: "boolean", description: "Remove local device credentials and agent log" },
53
+ all: { type: "boolean", description: "Remove the entire ~/.aximo directory" },
54
+ },
55
+ async run({ args }) {
56
+ const result = runDeviceReset({
57
+ python: Boolean(args.python),
58
+ deps: Boolean(args.deps),
59
+ device: Boolean(args.device),
60
+ all: Boolean(args.all),
61
+ });
62
+ printResult(result);
63
+ if (result.ok && args.device) {
64
+ console.error("\nNote: this only removed local credentials. The device row on the Aximo server still exists —\nrun `aximo device unregister` first if you want to delete it server-side.");
65
+ }
66
+ },
67
+ });
@@ -0,0 +1 @@
1
+ export {};