@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { runDeviceReset } from "./reset.js";
|
|
6
|
+
let tempHome;
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
tempHome = mkdtempSync(join(tmpdir(), "aximo-reset-"));
|
|
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
|
+
function seedAll() {
|
|
23
|
+
mkdirSync(join(tempHome, "python", "bin"), { recursive: true });
|
|
24
|
+
mkdirSync(join(tempHome, "cache"), { recursive: true });
|
|
25
|
+
writeFileSync(join(tempHome, "python.version"), "tag", { mode: 0o644 });
|
|
26
|
+
writeFileSync(join(tempHome, "python.deps.sha256"), "hash", { mode: 0o644 });
|
|
27
|
+
writeFileSync(join(tempHome, "device.json"), "{}", { mode: 0o600 });
|
|
28
|
+
writeFileSync(join(tempHome, "remote.log"), "log", { mode: 0o644 });
|
|
29
|
+
}
|
|
30
|
+
describe("runDeviceReset", () => {
|
|
31
|
+
it("rejects when no flags are passed", () => {
|
|
32
|
+
const result = runDeviceReset();
|
|
33
|
+
expect(result.ok).toBe(false);
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
expect(result.error.code).toBe("validation_error");
|
|
36
|
+
});
|
|
37
|
+
it("--python removes python dir, markers, and cache, leaves device config", () => {
|
|
38
|
+
seedAll();
|
|
39
|
+
const result = runDeviceReset({ python: true });
|
|
40
|
+
expect(result.ok).toBe(true);
|
|
41
|
+
expect(existsSync(join(tempHome, "python"))).toBe(false);
|
|
42
|
+
expect(existsSync(join(tempHome, "python.version"))).toBe(false);
|
|
43
|
+
expect(existsSync(join(tempHome, "python.deps.sha256"))).toBe(false);
|
|
44
|
+
expect(existsSync(join(tempHome, "cache"))).toBe(false);
|
|
45
|
+
expect(existsSync(join(tempHome, "device.json"))).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it("--deps removes only the deps marker", () => {
|
|
48
|
+
seedAll();
|
|
49
|
+
const result = runDeviceReset({ deps: true });
|
|
50
|
+
expect(result.ok).toBe(true);
|
|
51
|
+
expect(existsSync(join(tempHome, "python.deps.sha256"))).toBe(false);
|
|
52
|
+
expect(existsSync(join(tempHome, "python"))).toBe(true);
|
|
53
|
+
expect(existsSync(join(tempHome, "python.version"))).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("--device removes device.json and log, leaves python", () => {
|
|
56
|
+
seedAll();
|
|
57
|
+
const result = runDeviceReset({ device: true });
|
|
58
|
+
expect(result.ok).toBe(true);
|
|
59
|
+
expect(existsSync(join(tempHome, "device.json"))).toBe(false);
|
|
60
|
+
expect(existsSync(join(tempHome, "remote.log"))).toBe(false);
|
|
61
|
+
expect(existsSync(join(tempHome, "python"))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it("--all wipes ~/.aximo entirely", () => {
|
|
64
|
+
seedAll();
|
|
65
|
+
const result = runDeviceReset({ all: true });
|
|
66
|
+
expect(result.ok).toBe(true);
|
|
67
|
+
expect(existsSync(tempHome)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import type { Envelope } from "../../types.js";
|
|
3
|
+
export interface SetupResult {
|
|
4
|
+
apiReachable: boolean;
|
|
5
|
+
pythonInstalled: boolean;
|
|
6
|
+
deviceRegistered: boolean;
|
|
7
|
+
deviceId?: string;
|
|
8
|
+
deviceName?: string;
|
|
9
|
+
alreadyRegistered: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface RunDeviceSetupOptions {
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
interface SetupDeps extends ClientDeps {
|
|
15
|
+
ensurePython?: () => Promise<unknown>;
|
|
16
|
+
promptName?: () => Promise<string | null>;
|
|
17
|
+
}
|
|
18
|
+
export declare function runDeviceSetup(options?: RunDeviceSetupOptions, deps?: SetupDeps): Promise<Envelope<SetupResult>>;
|
|
19
|
+
declare const _default: import("citty").CommandDef<{
|
|
20
|
+
readonly name: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
readonly description: "Device name (skipped if already registered)";
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
export default _default;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { request } from "../../client.js";
|
|
5
|
+
import { handleApiError } from "../../errors.js";
|
|
6
|
+
import { formatError, formatSuccess, printResult } from "../../output.js";
|
|
7
|
+
import { runDeviceRegister } from "./register.js";
|
|
8
|
+
import { getDeviceConfigPath } from "./paths.js";
|
|
9
|
+
function loadExistingDevice() {
|
|
10
|
+
const configPath = getDeviceConfigPath();
|
|
11
|
+
if (!existsSync(configPath))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
15
|
+
if (typeof parsed.deviceId === "string" && typeof parsed.name === "string") {
|
|
16
|
+
return { deviceId: parsed.deviceId, name: parsed.name };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function promptDeviceName() {
|
|
25
|
+
if (!process.stdin.isTTY)
|
|
26
|
+
return null;
|
|
27
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
+
try {
|
|
29
|
+
const answer = (await rl.question("Device name: ")).trim();
|
|
30
|
+
return answer || null;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
rl.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function runDeviceSetup(options = {}, deps) {
|
|
37
|
+
try {
|
|
38
|
+
await request("/api/health", {}, deps ?? {});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return handleApiError(err);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
if (deps?.ensurePython) {
|
|
45
|
+
await deps.ensurePython();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const mod = await import("../../desktop-server/setup.js");
|
|
49
|
+
await mod.ensurePythonReady();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return formatError("api_error", `Python setup failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
54
|
+
}
|
|
55
|
+
const existing = loadExistingDevice();
|
|
56
|
+
if (existing) {
|
|
57
|
+
return formatSuccess({
|
|
58
|
+
apiReachable: true,
|
|
59
|
+
pythonInstalled: true,
|
|
60
|
+
deviceRegistered: true,
|
|
61
|
+
alreadyRegistered: true,
|
|
62
|
+
deviceId: existing.deviceId,
|
|
63
|
+
deviceName: existing.name,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
let name = options.name?.trim();
|
|
67
|
+
if (!name) {
|
|
68
|
+
const prompted = deps?.promptName ? await deps.promptName() : await promptDeviceName();
|
|
69
|
+
if (prompted)
|
|
70
|
+
name = prompted;
|
|
71
|
+
}
|
|
72
|
+
if (!name) {
|
|
73
|
+
return formatError("validation_error", "Device name required: pass --name, or run in an interactive terminal", false);
|
|
74
|
+
}
|
|
75
|
+
const registered = await runDeviceRegister({ name }, deps);
|
|
76
|
+
if (!registered.ok)
|
|
77
|
+
return registered;
|
|
78
|
+
return formatSuccess({
|
|
79
|
+
apiReachable: true,
|
|
80
|
+
pythonInstalled: true,
|
|
81
|
+
deviceRegistered: true,
|
|
82
|
+
alreadyRegistered: false,
|
|
83
|
+
deviceId: registered.data.deviceId,
|
|
84
|
+
deviceName: name,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export default defineCommand({
|
|
88
|
+
meta: {
|
|
89
|
+
name: "setup",
|
|
90
|
+
description: "First-time onboarding: checks API, installs Python, registers device",
|
|
91
|
+
},
|
|
92
|
+
args: {
|
|
93
|
+
name: { type: "string", description: "Device name (skipped if already registered)" },
|
|
94
|
+
},
|
|
95
|
+
async run({ args }) {
|
|
96
|
+
const options = {};
|
|
97
|
+
if (args.name !== undefined)
|
|
98
|
+
options.name = args.name;
|
|
99
|
+
const result = await runDeviceSetup(options);
|
|
100
|
+
printResult(result);
|
|
101
|
+
if (result.ok) {
|
|
102
|
+
console.error("\nNext: aximo device connect");
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { existsSync, 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 { runDeviceSetup } from "./setup.js";
|
|
7
|
+
let tempHome;
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
tempHome = mkdtempSync(join(tmpdir(), "aximo-setup-"));
|
|
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
|
+
const noopPython = async () => {
|
|
26
|
+
// no-op: skip real Python install in tests
|
|
27
|
+
};
|
|
28
|
+
describe("runDeviceSetup", () => {
|
|
29
|
+
it("fails with validation_error if no name and stdin is not a TTY", async () => {
|
|
30
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ status: "ok" })));
|
|
31
|
+
const result = await runDeviceSetup({}, { ensurePython: noopPython, promptName: async () => null });
|
|
32
|
+
expect(result.ok).toBe(false);
|
|
33
|
+
if (!result.ok)
|
|
34
|
+
expect(result.error.code).toBe("validation_error");
|
|
35
|
+
});
|
|
36
|
+
it("registers a new device and writes config", async () => {
|
|
37
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ status: "ok" })), http.post(`${TEST_BASE_URL}/api/devices/register`, () => HttpResponse.json({ deviceId: "dev-1", deviceToken: "tok-1" })));
|
|
38
|
+
const result = await runDeviceSetup({ name: "My PC" }, { ensurePython: noopPython });
|
|
39
|
+
expect(result.ok).toBe(true);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
return;
|
|
42
|
+
expect(result.data.deviceId).toBe("dev-1");
|
|
43
|
+
expect(result.data.alreadyRegistered).toBe(false);
|
|
44
|
+
expect(existsSync(join(tempHome, "device.json"))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it("reports alreadyRegistered and skips re-registration", async () => {
|
|
47
|
+
writeFileSync(join(tempHome, "device.json"), JSON.stringify({ deviceId: "existing-id", deviceToken: "t", name: "Old PC" }));
|
|
48
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ status: "ok" })));
|
|
49
|
+
const result = await runDeviceSetup({ name: "Ignored" }, { ensurePython: noopPython });
|
|
50
|
+
expect(result.ok).toBe(true);
|
|
51
|
+
if (!result.ok)
|
|
52
|
+
return;
|
|
53
|
+
expect(result.data.alreadyRegistered).toBe(true);
|
|
54
|
+
expect(result.data.deviceId).toBe("existing-id");
|
|
55
|
+
expect(result.data.deviceName).toBe("Old PC");
|
|
56
|
+
});
|
|
57
|
+
it("propagates a health check failure", async () => {
|
|
58
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ error: "boom" }, { status: 503 })));
|
|
59
|
+
const result = await runDeviceSetup({ name: "My PC" }, { ensurePython: noopPython });
|
|
60
|
+
expect(result.ok).toBe(false);
|
|
61
|
+
if (!result.ok)
|
|
62
|
+
expect(result.error.retryable).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import type { Envelope } from "../../types.js";
|
|
3
|
+
interface UserDevice {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
platform: string;
|
|
7
|
+
os?: string;
|
|
8
|
+
status: string;
|
|
9
|
+
lastSeenAt?: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function runDeviceStatus(deps?: ClientDeps): Promise<Envelope<{
|
|
13
|
+
devices: UserDevice[];
|
|
14
|
+
}>>;
|
|
15
|
+
declare const _default: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { formatSuccess, printResult } from "../../output.js";
|
|
5
|
+
export async function runDeviceStatus(deps) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await request("/api/devices/user", {}, deps ?? {});
|
|
8
|
+
const devices = res.devices ?? [];
|
|
9
|
+
return formatSuccess({ devices }, { total: devices.length });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return handleApiError(err);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export default defineCommand({
|
|
16
|
+
meta: { name: "status", description: "List your registered devices" },
|
|
17
|
+
async run() {
|
|
18
|
+
printResult(await runDeviceStatus());
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import type { Envelope } from "../../types.js";
|
|
3
|
+
export declare function runDeviceUnregister(options: {
|
|
4
|
+
deviceId?: string;
|
|
5
|
+
}, deps?: ClientDeps): Promise<Envelope<{
|
|
6
|
+
success: boolean;
|
|
7
|
+
}>>;
|
|
8
|
+
declare const _default: import("citty").CommandDef<{
|
|
9
|
+
readonly "device-id": {
|
|
10
|
+
readonly type: "string";
|
|
11
|
+
readonly description: "Device ID (defaults to local device)";
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { existsSync, unlinkSync, readFileSync } from "node:fs";
|
|
3
|
+
import { request } from "../../client.js";
|
|
4
|
+
import { handleApiError } from "../../errors.js";
|
|
5
|
+
import { formatSuccess, formatError, printResult } from "../../output.js";
|
|
6
|
+
import { getDeviceConfigPath } from "./paths.js";
|
|
7
|
+
export async function runDeviceUnregister(options, deps) {
|
|
8
|
+
let deviceId = options.deviceId;
|
|
9
|
+
if (!deviceId && existsSync(getDeviceConfigPath())) {
|
|
10
|
+
try {
|
|
11
|
+
const config = JSON.parse(readFileSync(getDeviceConfigPath(), "utf-8"));
|
|
12
|
+
if (config &&
|
|
13
|
+
typeof config === "object" &&
|
|
14
|
+
typeof config.deviceId === "string") {
|
|
15
|
+
deviceId = config.deviceId;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
return formatError("validation_error", "Device config is malformed: missing deviceId", false);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return formatError("validation_error", "Failed to read device config", false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!deviceId) {
|
|
26
|
+
return formatError("validation_error", "No device ID provided and no local device config found. " +
|
|
27
|
+
"Pass a device ID or register first with: aximo device register", false);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await request(`/api/devices/user/${deviceId}`, { method: "DELETE" }, deps ?? {});
|
|
31
|
+
if (existsSync(getDeviceConfigPath())) {
|
|
32
|
+
try {
|
|
33
|
+
const config = JSON.parse(readFileSync(getDeviceConfigPath(), "utf-8"));
|
|
34
|
+
if (config.deviceId === deviceId) {
|
|
35
|
+
unlinkSync(getDeviceConfigPath());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// non-fatal
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return formatSuccess({ success: true });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return handleApiError(err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export default defineCommand({
|
|
49
|
+
meta: { name: "unregister", description: "Unregister a device" },
|
|
50
|
+
args: {
|
|
51
|
+
"device-id": {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Device ID (defaults to local device)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
async run({ args }) {
|
|
57
|
+
const deviceId = args["device-id"];
|
|
58
|
+
printResult(await runDeviceUnregister(deviceId ? { deviceId } : {}));
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Envelope, HealthStatus } from "../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../client.js";
|
|
3
|
+
export declare function runHealthCheck(deps?: ClientDeps): Promise<Envelope<HealthStatus>>;
|
|
4
|
+
declare const _default: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult } from "../../output.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { request } from "../../client.js";
|
|
5
|
+
export async function runHealthCheck(deps) {
|
|
6
|
+
try {
|
|
7
|
+
// /api/health may return empty body; we normalize to { status: "ok" }
|
|
8
|
+
await request("/api/health", {}, deps ?? {});
|
|
9
|
+
return formatSuccess({ status: "ok" });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return handleApiError(err);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export default defineCommand({
|
|
16
|
+
meta: { name: "check", description: "Check Aximo API health" },
|
|
17
|
+
async run() {
|
|
18
|
+
printResult(await runHealthCheck());
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { server, http, HttpResponse, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runHealthCheck } from "./check.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("runHealthCheck", () => {
|
|
12
|
+
it("returns ok envelope on 200", async () => {
|
|
13
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, () => new HttpResponse(null, { status: 200 })));
|
|
14
|
+
const result = await runHealthCheck();
|
|
15
|
+
expect(result).toEqual({ ok: true, data: { status: "ok" } });
|
|
16
|
+
});
|
|
17
|
+
it("maps 500 to api_error retryable", async () => {
|
|
18
|
+
server.use(http.get(`${TEST_BASE_URL}/api/health`, () => HttpResponse.json({ error: "Internal" }, { status: 500 })));
|
|
19
|
+
const result = await runHealthCheck();
|
|
20
|
+
expect(result.ok).toBe(false);
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
expect(result.error.code).toBe("api_error");
|
|
23
|
+
expect(result.error.retryable).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Envelope, Project } from "../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../client.js";
|
|
3
|
+
export declare function runProjectList(deps?: ClientDeps): Promise<Envelope<{
|
|
4
|
+
projects: Project[];
|
|
5
|
+
}>>;
|
|
6
|
+
declare const _default: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult } from "../../output.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { request } from "../../client.js";
|
|
5
|
+
export async function runProjectList(deps) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await request("/api/organizations/projects", {}, deps ?? {});
|
|
8
|
+
const projects = res.projects ?? [];
|
|
9
|
+
return formatSuccess({ projects }, { total: projects.length });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return handleApiError(err);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export default defineCommand({
|
|
16
|
+
meta: { name: "list", description: "List accessible Aximo projects" },
|
|
17
|
+
async run() {
|
|
18
|
+
printResult(await runProjectList());
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { server, http, HttpResponse, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runProjectList } 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("runProjectList", () => {
|
|
12
|
+
it("returns projects + total meta", async () => {
|
|
13
|
+
server.use(http.get(`${TEST_BASE_URL}/api/organizations/projects`, () => HttpResponse.json({ projects: [{ id: "p1", name: "Foo" }] })));
|
|
14
|
+
const result = await runProjectList();
|
|
15
|
+
expect(result.ok).toBe(true);
|
|
16
|
+
if (result.ok) {
|
|
17
|
+
expect(result.data.projects).toHaveLength(1);
|
|
18
|
+
expect(result.meta?.total).toBe(1);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
it("maps 401 to unauthorized", async () => {
|
|
22
|
+
server.use(http.get(`${TEST_BASE_URL}/api/organizations/projects`, () => HttpResponse.json({ error: "Unauthorized" }, { status: 401 })));
|
|
23
|
+
const result = await runProjectList();
|
|
24
|
+
expect(result.ok).toBe(false);
|
|
25
|
+
if (!result.ok)
|
|
26
|
+
expect(result.error.code).toBe("unauthorized");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface ArgSchema {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
description: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface CommandSchema {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
args: ArgSchema[];
|
|
11
|
+
subCommands?: CommandSchema[];
|
|
12
|
+
}
|
|
13
|
+
interface Schema {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
commands: CommandSchema[];
|
|
17
|
+
}
|
|
18
|
+
export declare function getSchema(): Schema;
|
|
19
|
+
export declare function lookupCommand(path: string): CommandSchema | undefined;
|
|
20
|
+
declare const _default: import("citty").CommandDef<{
|
|
21
|
+
readonly command: {
|
|
22
|
+
readonly type: "positional";
|
|
23
|
+
readonly description: "Command path (e.g. session.create)";
|
|
24
|
+
readonly required: false;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
export default _default;
|