kiwivm-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 +118 -0
- package/dist/admin-fOud1ZmX.mjs +15 -0
- package/dist/admin-fOud1ZmX.mjs.map +1 -0
- package/dist/backup-D1UJ4aap.mjs +12 -0
- package/dist/backup-D1UJ4aap.mjs.map +1 -0
- package/dist/help-Dk-WApoi.mjs +40 -0
- package/dist/help-Dk-WApoi.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +177 -0
- package/dist/index.mjs.map +1 -0
- package/dist/info-DKExtFYH.mjs +13 -0
- package/dist/info-DKExtFYH.mjs.map +1 -0
- package/dist/monitoring-BSuv8fj9.mjs +13 -0
- package/dist/monitoring-BSuv8fj9.mjs.map +1 -0
- package/dist/network-1ycEIJqT.mjs +15 -0
- package/dist/network-1ycEIJqT.mjs.map +1 -0
- package/dist/power-CDg0Mx1A.mjs +14 -0
- package/dist/power-CDg0Mx1A.mjs.map +1 -0
- package/dist/snapshot-LO_ufoj5.mjs +23 -0
- package/dist/snapshot-LO_ufoj5.mjs.map +1 -0
- package/dist/system-Bl-dsqX9.mjs +21 -0
- package/dist/system-Bl-dsqX9.mjs.map +1 -0
- package/package.json +46 -0
- package/src/client.test.ts +68 -0
- package/src/client.ts +55 -0
- package/src/commands/admin.test.ts +65 -0
- package/src/commands/admin.ts +25 -0
- package/src/commands/backup.test.ts +66 -0
- package/src/commands/backup.ts +23 -0
- package/src/commands/help.test.ts +50 -0
- package/src/commands/help.ts +36 -0
- package/src/commands/info.test.ts +67 -0
- package/src/commands/info.ts +20 -0
- package/src/commands/monitoring.test.ts +82 -0
- package/src/commands/monitoring.ts +20 -0
- package/src/commands/network.test.ts +85 -0
- package/src/commands/network.ts +24 -0
- package/src/commands/power.test.ts +68 -0
- package/src/commands/power.ts +22 -0
- package/src/commands/snapshot.test.ts +159 -0
- package/src/commands/snapshot.ts +40 -0
- package/src/commands/system.test.ts +98 -0
- package/src/commands/system.ts +29 -0
- package/src/index.test.ts +375 -0
- package/src/index.ts +172 -0
- package/src/types.ts +94 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { KiwiVMClient } from "./client.ts";
|
|
3
|
+
import { KiwiVMError } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
global.fetch = vi.fn();
|
|
6
|
+
|
|
7
|
+
const client = new KiwiVMClient({
|
|
8
|
+
veid: "12345",
|
|
9
|
+
apiKey: "test-key",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("KiwiVMClient", () => {
|
|
13
|
+
afterEach(() => vi.resetAllMocks());
|
|
14
|
+
|
|
15
|
+
it("sends POST with form-encoded body and returns success", async () => {
|
|
16
|
+
vi.mocked(fetch).mockResolvedValueOnce({
|
|
17
|
+
ok: true,
|
|
18
|
+
json: () => Promise.resolve({ error: 0 }),
|
|
19
|
+
} as Response);
|
|
20
|
+
|
|
21
|
+
const result = await client.call("restart");
|
|
22
|
+
expect(result.error).toBe(0);
|
|
23
|
+
|
|
24
|
+
const call = vi.mocked(fetch).mock.calls[0];
|
|
25
|
+
expect(call?.[0]).toBe("https://api.64clouds.com/v1/restart");
|
|
26
|
+
expect((call?.[1] as RequestInit).method).toBe("POST");
|
|
27
|
+
expect((call?.[1] as RequestInit).headers).toEqual({
|
|
28
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const body = (call?.[1] as RequestInit).body as URLSearchParams;
|
|
32
|
+
expect(body.get("veid")).toBe("12345");
|
|
33
|
+
expect(body.get("api_key")).toBe("test-key");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("passes additional params to request body", async () => {
|
|
37
|
+
vi.mocked(fetch).mockResolvedValueOnce({
|
|
38
|
+
ok: true,
|
|
39
|
+
json: () => Promise.resolve({ error: 0 }),
|
|
40
|
+
} as Response);
|
|
41
|
+
|
|
42
|
+
await client.call("snapshot/create", { description: "test" });
|
|
43
|
+
|
|
44
|
+
const body = (vi.mocked(fetch).mock.calls[0]?.[1] as RequestInit)
|
|
45
|
+
.body as URLSearchParams;
|
|
46
|
+
expect(body.get("description")).toBe("test");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("throws KiwiVMError on API error", async () => {
|
|
50
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
51
|
+
ok: true,
|
|
52
|
+
json: () => Promise.resolve({ error: 1, message: "Invalid VEID" }),
|
|
53
|
+
} as Response);
|
|
54
|
+
|
|
55
|
+
await expect(client.call("restart")).rejects.toThrow(KiwiVMError);
|
|
56
|
+
await expect(client.call("restart")).rejects.toThrow("Invalid VEID");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("throws on HTTP error", async () => {
|
|
60
|
+
vi.mocked(fetch).mockResolvedValueOnce({
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 500,
|
|
63
|
+
statusText: "Internal Server Error",
|
|
64
|
+
} as Response);
|
|
65
|
+
|
|
66
|
+
await expect(client.call("restart")).rejects.toThrow("HTTP 500");
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { KiwiVMResponse } from "./types.ts";
|
|
2
|
+
import { KiwiVMError } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
const API_BASE = "https://api.64clouds.com/v1";
|
|
5
|
+
|
|
6
|
+
export interface ClientOptions {
|
|
7
|
+
veid: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class KiwiVMClient {
|
|
12
|
+
private veid: string;
|
|
13
|
+
private apiKey: string;
|
|
14
|
+
|
|
15
|
+
constructor(options: ClientOptions) {
|
|
16
|
+
this.veid = options.veid;
|
|
17
|
+
this.apiKey = options.apiKey;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async call<T extends KiwiVMResponse = KiwiVMResponse>(
|
|
21
|
+
endpoint: string,
|
|
22
|
+
params: Record<string, string | number | boolean | undefined> = {},
|
|
23
|
+
): Promise<T> {
|
|
24
|
+
const body = new URLSearchParams();
|
|
25
|
+
body.set("veid", this.veid);
|
|
26
|
+
body.set("api_key", this.apiKey);
|
|
27
|
+
for (const [key, value] of Object.entries(params)) {
|
|
28
|
+
if (value !== undefined) {
|
|
29
|
+
body.set(key, String(value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const response = await fetch(`${API_BASE}/${endpoint}`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = (await response.json()) as T & KiwiVMResponse;
|
|
44
|
+
|
|
45
|
+
if (data.error !== 0) {
|
|
46
|
+
throw new KiwiVMError(
|
|
47
|
+
data.message ?? `API error: ${data.error}`,
|
|
48
|
+
data.error,
|
|
49
|
+
data,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data as T;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { run } from "./admin.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("admin command", () => {
|
|
11
|
+
describe("suspensions", () => {
|
|
12
|
+
it("calls getSuspensionDetails", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
call.mockResolvedValueOnce({
|
|
15
|
+
error: 0,
|
|
16
|
+
suspensions: [{ reason: "abuse", createdAt: "2025-01-01" }],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = await run("suspensions", {}, client);
|
|
20
|
+
|
|
21
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith(
|
|
22
|
+
"getSuspensionDetails",
|
|
23
|
+
);
|
|
24
|
+
expect(result).toMatchObject({
|
|
25
|
+
suspensions: [{ reason: "abuse" }],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("unsuspend", () => {
|
|
31
|
+
it("calls unsuspend with record-id flag", async () => {
|
|
32
|
+
const { client, call } = mockClient();
|
|
33
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
34
|
+
|
|
35
|
+
await run("unsuspend", { recordId: "42" }, client);
|
|
36
|
+
|
|
37
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("unsuspend", {
|
|
38
|
+
recordId: "42",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("resolve", () => {
|
|
44
|
+
it("calls resolvePolicyViolation with record-id flag", async () => {
|
|
45
|
+
const { client, call } = mockClient();
|
|
46
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
47
|
+
|
|
48
|
+
await run("resolve", { recordId: "42" }, client);
|
|
49
|
+
|
|
50
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith(
|
|
51
|
+
"resolvePolicyViolation",
|
|
52
|
+
{ recordId: "42" },
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("propagates errors from the client", async () => {
|
|
58
|
+
const { client, call } = mockClient();
|
|
59
|
+
call.mockRejectedValueOnce(new Error("Record not found"));
|
|
60
|
+
|
|
61
|
+
await expect(run("unsuspend", { recordId: "999" }, client)).rejects.toThrow(
|
|
62
|
+
"Record not found",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
2
|
+
|
|
3
|
+
export async function run(
|
|
4
|
+
action: string,
|
|
5
|
+
flags: Record<string, string>,
|
|
6
|
+
client: KiwiVMClient,
|
|
7
|
+
): Promise<unknown> {
|
|
8
|
+
switch (action) {
|
|
9
|
+
case "suspensions":
|
|
10
|
+
return client.call("getSuspensionDetails");
|
|
11
|
+
case "unsuspend":
|
|
12
|
+
return client.call("unsuspend", { recordId: flags["recordId"] });
|
|
13
|
+
case "resolve":
|
|
14
|
+
if (flags["recordId"] !== undefined) {
|
|
15
|
+
return client.call("resolvePolicyViolation", {
|
|
16
|
+
recordId: flags["recordId"],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return client.call("getPolicyViolations");
|
|
20
|
+
default:
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Unknown admin action: ${action}. Valid: suspensions, unsuspend, resolve`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { run } from "./backup.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("backup command", () => {
|
|
11
|
+
describe("list", () => {
|
|
12
|
+
it("calls backup/list", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
const backups = {
|
|
15
|
+
error: 0,
|
|
16
|
+
backups: [
|
|
17
|
+
{
|
|
18
|
+
backupToken: "tok1",
|
|
19
|
+
size: 1024,
|
|
20
|
+
os: "Ubuntu",
|
|
21
|
+
md5: "abc",
|
|
22
|
+
timestamp: 1700000000,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
call.mockResolvedValueOnce(backups);
|
|
27
|
+
|
|
28
|
+
const result = await run("list", {}, client);
|
|
29
|
+
|
|
30
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("backup/list");
|
|
31
|
+
expect(result).toEqual(backups);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not pass any extra params", async () => {
|
|
35
|
+
const { client, call } = mockClient();
|
|
36
|
+
call.mockResolvedValueOnce({ error: 0, backups: [] });
|
|
37
|
+
|
|
38
|
+
await run("list", {}, client);
|
|
39
|
+
|
|
40
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("backup/list");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("copy", () => {
|
|
45
|
+
it("calls backup/copyToSnapshot with backupToken", async () => {
|
|
46
|
+
const { client, call } = mockClient();
|
|
47
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
48
|
+
|
|
49
|
+
await run("copy", { backupToken: "tok1" }, client);
|
|
50
|
+
|
|
51
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith(
|
|
52
|
+
"backup/copyToSnapshot",
|
|
53
|
+
{ backupToken: "tok1" },
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("propagates errors from the client", async () => {
|
|
59
|
+
const { client, call } = mockClient();
|
|
60
|
+
call.mockRejectedValueOnce(new Error("Backup not found"));
|
|
61
|
+
|
|
62
|
+
await expect(run("copy", { backupToken: "bad" }, client)).rejects.toThrow(
|
|
63
|
+
"Backup not found",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
2
|
+
import type { Backup, KiwiVMResponse } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
interface BackupListResponse extends KiwiVMResponse {
|
|
5
|
+
backups: Backup[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function run(
|
|
9
|
+
action: string,
|
|
10
|
+
flags: Record<string, string>,
|
|
11
|
+
client: KiwiVMClient,
|
|
12
|
+
): Promise<unknown> {
|
|
13
|
+
switch (action) {
|
|
14
|
+
case "list":
|
|
15
|
+
return client.call<BackupListResponse>("backup/list");
|
|
16
|
+
case "copy":
|
|
17
|
+
return client.call("backup/copyToSnapshot", {
|
|
18
|
+
backupToken: flags["backupToken"],
|
|
19
|
+
});
|
|
20
|
+
default:
|
|
21
|
+
throw new Error(`Unknown backup action: ${action}. Valid: list, copy`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { run } from "./help.ts";
|
|
3
|
+
|
|
4
|
+
describe("help command", () => {
|
|
5
|
+
it("returns a string containing usage information", async () => {
|
|
6
|
+
const result = await run();
|
|
7
|
+
|
|
8
|
+
expect(typeof result).toBe("string");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("mentions the CLI name", async () => {
|
|
12
|
+
const result = await run();
|
|
13
|
+
|
|
14
|
+
expect(result).toContain("kiwivm-cli");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("lists available command categories", async () => {
|
|
18
|
+
const result = await run();
|
|
19
|
+
|
|
20
|
+
// Spot-check a few categories
|
|
21
|
+
expect(result).toContain("power");
|
|
22
|
+
expect(result).toContain("info");
|
|
23
|
+
expect(result).toContain("snapshot");
|
|
24
|
+
expect(result).toContain("backup");
|
|
25
|
+
expect(result).toContain("system");
|
|
26
|
+
expect(result).toContain("network");
|
|
27
|
+
expect(result).toContain("monitoring");
|
|
28
|
+
expect(result).toContain("admin");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("mentions global flags --veid and --api-key", async () => {
|
|
32
|
+
const result = await run();
|
|
33
|
+
|
|
34
|
+
expect(result).toContain("--veid");
|
|
35
|
+
expect(result).toContain("--api-key");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("mentions environment variables", async () => {
|
|
39
|
+
const result = await run();
|
|
40
|
+
|
|
41
|
+
expect(result).toContain("KIWIVM_VEID");
|
|
42
|
+
expect(result).toContain("KIWIVM_API_KEY");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns a multi-line help text", async () => {
|
|
46
|
+
const result = await run();
|
|
47
|
+
|
|
48
|
+
expect(result).toContain("\n");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const HELP = `Usage: kiwivm-cli <category> <action> [--flags...]
|
|
2
|
+
|
|
3
|
+
Auth: --veid <VEID> --api-key <KEY> (or KIWIVM_VEID / KIWIVM_API_KEY env vars)
|
|
4
|
+
|
|
5
|
+
Categories:
|
|
6
|
+
|
|
7
|
+
power start | stop | restart | kill
|
|
8
|
+
info (no action) | live
|
|
9
|
+
snapshot create | list | delete | restore | sticky | export | import
|
|
10
|
+
backup list | copy
|
|
11
|
+
system hostname | rdns | password | sshkey | os | reinstall
|
|
12
|
+
network ipv6-add | ipv6-delete | private-list | private-assign | private-delete
|
|
13
|
+
monitoring usage | audit | rate-limit
|
|
14
|
+
admin suspensions | unsuspend | resolve | resolve-violation
|
|
15
|
+
|
|
16
|
+
Flags:
|
|
17
|
+
|
|
18
|
+
--description Snapshot description (snapshot create)
|
|
19
|
+
--snapshot Snapshot file name (snapshot delete/restore/sticky/export)
|
|
20
|
+
--sticky 0 or 1 (snapshot sticky)
|
|
21
|
+
--source-veid Source VEID (snapshot import)
|
|
22
|
+
--source-token Source token (snapshot import)
|
|
23
|
+
--backup-token Backup token (backup copy)
|
|
24
|
+
--new-hostname New hostname (system hostname)
|
|
25
|
+
--ip IP address (system rdns, network ipv6-delete/private-assign/private-delete)
|
|
26
|
+
--ptr PTR/rDNS record (system rdns)
|
|
27
|
+
--ssh-keys SSH keys (system sshkey update)
|
|
28
|
+
--os OS template name (system reinstall)
|
|
29
|
+
--record-id Record ID (admin unsuspend/resolve-violation)
|
|
30
|
+
|
|
31
|
+
Output: JSON to stdout. Errors to stderr with exit code 1.
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export async function run(): Promise<string> {
|
|
35
|
+
return HELP;
|
|
36
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { run } from "./info.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("info command", () => {
|
|
11
|
+
it("calls getServiceInfo for basic info (empty action)", async () => {
|
|
12
|
+
const { client, call } = mockClient();
|
|
13
|
+
call.mockResolvedValueOnce({
|
|
14
|
+
error: 0,
|
|
15
|
+
hostname: "my-vps",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const result = await run("", {}, client);
|
|
19
|
+
|
|
20
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("getServiceInfo");
|
|
21
|
+
expect(result).toMatchObject({ hostname: "my-vps" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("calls getServiceInfo when action is omitted", async () => {
|
|
25
|
+
const { client, call } = mockClient();
|
|
26
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
27
|
+
|
|
28
|
+
await run("", {}, client);
|
|
29
|
+
|
|
30
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("getServiceInfo");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("calls getLiveServiceInfo for live action", async () => {
|
|
34
|
+
const { client, call } = mockClient();
|
|
35
|
+
call.mockResolvedValueOnce({
|
|
36
|
+
error: 0,
|
|
37
|
+
ve_status: "Running",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const result = await run("live", {}, client);
|
|
41
|
+
|
|
42
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("getLiveServiceInfo");
|
|
43
|
+
expect(result).toMatchObject({ ve_status: "Running" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns the complete API response", async () => {
|
|
47
|
+
const { client, call } = mockClient();
|
|
48
|
+
const apiResponse = {
|
|
49
|
+
error: 0,
|
|
50
|
+
hostname: "my-vps",
|
|
51
|
+
plan_disk: 50,
|
|
52
|
+
plan_ram: 1024,
|
|
53
|
+
};
|
|
54
|
+
call.mockResolvedValueOnce(apiResponse);
|
|
55
|
+
|
|
56
|
+
const result = await run("", {}, client);
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual(apiResponse);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("propagates errors from the client", async () => {
|
|
62
|
+
const { client, call } = mockClient();
|
|
63
|
+
call.mockRejectedValueOnce(new Error("Network error"));
|
|
64
|
+
|
|
65
|
+
await expect(run("live", {}, client)).rejects.toThrow("Network error");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
2
|
+
import type { LiveServiceInfo, ServiceInfo } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export async function run(
|
|
5
|
+
action: string,
|
|
6
|
+
_flags: Record<string, string>,
|
|
7
|
+
client: KiwiVMClient,
|
|
8
|
+
): Promise<unknown> {
|
|
9
|
+
switch (action) {
|
|
10
|
+
case "live":
|
|
11
|
+
return client.call<LiveServiceInfo>("getLiveServiceInfo");
|
|
12
|
+
case "":
|
|
13
|
+
case "info":
|
|
14
|
+
return client.call<ServiceInfo>("getServiceInfo");
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Unknown info action: ${action}. Valid: (no action), live`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { run } from "./monitoring.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("monitoring command", () => {
|
|
11
|
+
describe("usage", () => {
|
|
12
|
+
it("calls getRawUsageStats", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
call.mockResolvedValueOnce({
|
|
15
|
+
error: 0,
|
|
16
|
+
data: [{ month: "2025-01", usage: 100 }],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = await run("usage", {}, client);
|
|
20
|
+
|
|
21
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("getRawUsageStats");
|
|
22
|
+
expect(result).toMatchObject({
|
|
23
|
+
data: [{ month: "2025-01", usage: 100 }],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("audit", () => {
|
|
29
|
+
it("calls getAuditLog", async () => {
|
|
30
|
+
const { client, call } = mockClient();
|
|
31
|
+
call.mockResolvedValueOnce({
|
|
32
|
+
error: 0,
|
|
33
|
+
logs: [{ timestamp: 1700000000, action: "restart" }],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await run("audit", {}, client);
|
|
37
|
+
|
|
38
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("getAuditLog");
|
|
39
|
+
expect(result).toMatchObject({
|
|
40
|
+
logs: [{ timestamp: 1700000000, action: "restart" }],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("rate-limit", () => {
|
|
46
|
+
it("calls getRateLimitStatus", async () => {
|
|
47
|
+
const { client, call } = mockClient();
|
|
48
|
+
call.mockResolvedValueOnce({
|
|
49
|
+
error: 0,
|
|
50
|
+
remaining: 950,
|
|
51
|
+
limit: 1000,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await run("rate-limit", {}, client);
|
|
55
|
+
|
|
56
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("getRateLimitStatus");
|
|
57
|
+
expect(result).toMatchObject({ remaining: 950, limit: 1000 });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns the complete API response", async () => {
|
|
62
|
+
const { client, call } = mockClient();
|
|
63
|
+
const apiResponse = {
|
|
64
|
+
error: 0,
|
|
65
|
+
remaining: 500,
|
|
66
|
+
limit: 1000,
|
|
67
|
+
reset: 1700000000,
|
|
68
|
+
};
|
|
69
|
+
call.mockResolvedValueOnce(apiResponse);
|
|
70
|
+
|
|
71
|
+
const result = await run("rate-limit", {}, client);
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual(apiResponse);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("propagates errors from the client", async () => {
|
|
77
|
+
const { client, call } = mockClient();
|
|
78
|
+
call.mockRejectedValueOnce(new Error("Rate limited"));
|
|
79
|
+
|
|
80
|
+
await expect(run("audit", {}, client)).rejects.toThrow("Rate limited");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
2
|
+
|
|
3
|
+
export async function run(
|
|
4
|
+
action: string,
|
|
5
|
+
_flags: Record<string, string>,
|
|
6
|
+
client: KiwiVMClient,
|
|
7
|
+
): Promise<unknown> {
|
|
8
|
+
switch (action) {
|
|
9
|
+
case "usage":
|
|
10
|
+
return client.call("getRawUsageStats");
|
|
11
|
+
case "audit":
|
|
12
|
+
return client.call("getAuditLog");
|
|
13
|
+
case "rate-limit":
|
|
14
|
+
return client.call("getRateLimitStatus");
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Unknown monitoring action: ${action}. Valid: usage, audit, rate-limit`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { run } from "./network.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("network command", () => {
|
|
11
|
+
describe("ipv6-add", () => {
|
|
12
|
+
it("calls ipv6/add with no extra params", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
15
|
+
|
|
16
|
+
const result = await run("ipv6-add", {}, client);
|
|
17
|
+
|
|
18
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("ipv6/add");
|
|
19
|
+
expect(result).toEqual({ error: 0 });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("ipv6-delete", () => {
|
|
24
|
+
it("calls ipv6/delete with ip flag", async () => {
|
|
25
|
+
const { client, call } = mockClient();
|
|
26
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
27
|
+
|
|
28
|
+
await run("ipv6-delete", { ip: "2001:db8::1" }, client);
|
|
29
|
+
|
|
30
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("ipv6/delete", {
|
|
31
|
+
ip: "2001:db8::1",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("private-list", () => {
|
|
37
|
+
it("calls privateIp/getAvailableIps", async () => {
|
|
38
|
+
const { client, call } = mockClient();
|
|
39
|
+
call.mockResolvedValueOnce({
|
|
40
|
+
error: 0,
|
|
41
|
+
availableIps: ["10.0.0.1"],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const result = await run("private-list", {}, client);
|
|
45
|
+
|
|
46
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith(
|
|
47
|
+
"privateIp/getAvailableIps",
|
|
48
|
+
);
|
|
49
|
+
expect(result).toMatchObject({ availableIps: ["10.0.0.1"] });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("private-assign", () => {
|
|
54
|
+
it("calls privateIp/assign with ip flag", async () => {
|
|
55
|
+
const { client, call } = mockClient();
|
|
56
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
57
|
+
|
|
58
|
+
await run("private-assign", { ip: "10.0.0.1" }, client);
|
|
59
|
+
|
|
60
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("privateIp/assign", {
|
|
61
|
+
ip: "10.0.0.1",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("private-delete", () => {
|
|
67
|
+
it("calls privateIp/delete with ip flag", async () => {
|
|
68
|
+
const { client, call } = mockClient();
|
|
69
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
70
|
+
|
|
71
|
+
await run("private-delete", { ip: "10.0.0.1" }, client);
|
|
72
|
+
|
|
73
|
+
expect(client.call).toHaveBeenCalledExactlyOnceWith("privateIp/delete", {
|
|
74
|
+
ip: "10.0.0.1",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("propagates errors from the client", async () => {
|
|
80
|
+
const { client, call } = mockClient();
|
|
81
|
+
call.mockRejectedValueOnce(new Error("Invalid IP"));
|
|
82
|
+
|
|
83
|
+
await expect(run("ipv6-add", {}, client)).rejects.toThrow("Invalid IP");
|
|
84
|
+
});
|
|
85
|
+
});
|