kiwivm-cli 0.1.0 → 0.2.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 (52) hide show
  1. package/README.md +62 -36
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +370 -50
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/commands/admin.test.ts +135 -22
  7. package/src/commands/admin.ts +57 -19
  8. package/src/commands/backup.test.ts +26 -23
  9. package/src/commands/backup.ts +13 -15
  10. package/src/commands/help.test.ts +27 -7
  11. package/src/commands/help.ts +61 -26
  12. package/src/commands/info.test.ts +47 -43
  13. package/src/commands/info.ts +11 -13
  14. package/src/commands/iso.test.ts +58 -0
  15. package/src/commands/iso.ts +21 -0
  16. package/src/commands/migrate.test.ts +105 -0
  17. package/src/commands/migrate.ts +38 -0
  18. package/src/commands/network.test.ts +107 -30
  19. package/src/commands/network.ts +56 -18
  20. package/src/commands/power.test.ts +58 -40
  21. package/src/commands/power.ts +27 -16
  22. package/src/commands/shell.test.ts +66 -0
  23. package/src/commands/shell.ts +25 -0
  24. package/src/commands/snapshot.test.ts +141 -71
  25. package/src/commands/snapshot.ts +85 -33
  26. package/src/commands/stats.test.ts +81 -0
  27. package/src/commands/stats.ts +25 -0
  28. package/src/commands/system.test.ts +109 -40
  29. package/src/commands/system.ts +55 -23
  30. package/src/index.test.ts +435 -148
  31. package/src/index.ts +129 -57
  32. package/src/types.ts +57 -1
  33. package/dist/admin-fOud1ZmX.mjs +0 -15
  34. package/dist/admin-fOud1ZmX.mjs.map +0 -1
  35. package/dist/backup-D1UJ4aap.mjs +0 -12
  36. package/dist/backup-D1UJ4aap.mjs.map +0 -1
  37. package/dist/help-Dk-WApoi.mjs +0 -40
  38. package/dist/help-Dk-WApoi.mjs.map +0 -1
  39. package/dist/info-DKExtFYH.mjs +0 -13
  40. package/dist/info-DKExtFYH.mjs.map +0 -1
  41. package/dist/monitoring-BSuv8fj9.mjs +0 -13
  42. package/dist/monitoring-BSuv8fj9.mjs.map +0 -1
  43. package/dist/network-1ycEIJqT.mjs +0 -15
  44. package/dist/network-1ycEIJqT.mjs.map +0 -1
  45. package/dist/power-CDg0Mx1A.mjs +0 -14
  46. package/dist/power-CDg0Mx1A.mjs.map +0 -1
  47. package/dist/snapshot-LO_ufoj5.mjs +0 -23
  48. package/dist/snapshot-LO_ufoj5.mjs.map +0 -1
  49. package/dist/system-Bl-dsqX9.mjs +0 -21
  50. package/dist/system-Bl-dsqX9.mjs.map +0 -1
  51. package/src/commands/monitoring.test.ts +0 -82
  52. package/src/commands/monitoring.ts +0 -20
@@ -1,40 +1,92 @@
1
1
  import type { KiwiVMClient } from "../client.ts";
2
- import type { KiwiVMResponse, Snapshot } from "../types.ts";
3
2
 
4
- export async function run(
5
- action: string,
3
+ export async function list(
4
+ _args: string[],
5
+ _flags: Record<string, string>,
6
+ client: KiwiVMClient,
7
+ ): Promise<unknown> {
8
+ return client.call("snapshot/list");
9
+ }
10
+
11
+ export async function create(
12
+ _args: string[],
6
13
  flags: Record<string, string>,
7
14
  client: KiwiVMClient,
8
15
  ): Promise<unknown> {
9
- switch (action) {
10
- case "create":
11
- return client.call("snapshot/create", {
12
- description: flags["description"],
13
- });
14
- case "list":
15
- return client.call<KiwiVMResponse & { snapshots: Snapshot[] }>(
16
- "snapshot/list",
17
- );
18
- case "delete":
19
- return client.call("snapshot/delete", { snapshot: flags["snapshot"] });
20
- case "restore":
21
- return client.call("snapshot/restore", { snapshot: flags["snapshot"] });
22
- case "sticky":
23
- return client.call("snapshot/toggleSticky", {
24
- snapshot: flags["snapshot"],
25
- sticky:
26
- flags["sticky"] !== undefined ? Number(flags["sticky"]) : undefined,
27
- });
28
- case "export":
29
- return client.call("snapshot/export", { snapshot: flags["snapshot"] });
30
- case "import":
31
- return client.call("snapshot/import", {
32
- sourceVeid: flags["sourceVeid"],
33
- sourceToken: flags["sourceToken"],
34
- });
35
- default:
36
- throw new Error(
37
- `Unknown snapshot action: ${action}. Valid: create, list, delete, restore, sticky, export, import`,
38
- );
16
+ return client.call("snapshot/create", {
17
+ description: flags["desc"] || flags["description"],
18
+ });
19
+ }
20
+
21
+ export async function deleteSnapshot(
22
+ args: string[],
23
+ _flags: Record<string, string>,
24
+ client: KiwiVMClient,
25
+ ): Promise<unknown> {
26
+ const token = args[0];
27
+ if (!token) {
28
+ throw new Error("snapshot delete requires a <token> argument");
29
+ }
30
+ return client.call("snapshot/delete", { snapshot: token });
31
+ }
32
+
33
+ export async function restore(
34
+ args: string[],
35
+ _flags: Record<string, string>,
36
+ client: KiwiVMClient,
37
+ ): Promise<unknown> {
38
+ const token = args[0];
39
+ if (!token) {
40
+ throw new Error("snapshot restore requires a <token> argument");
41
+ }
42
+ return client.call("snapshot/restore", { snapshot: token });
43
+ }
44
+
45
+ export async function sticky(
46
+ args: string[],
47
+ flags: Record<string, string>,
48
+ client: KiwiVMClient,
49
+ ): Promise<unknown> {
50
+ const token = args[0];
51
+ if (!token) {
52
+ throw new Error("snapshot sticky requires a <token> argument");
53
+ }
54
+ if (flags["on"] !== undefined && flags["off"] !== undefined) {
55
+ throw new Error("snapshot sticky requires exactly one of --on or --off");
56
+ }
57
+ if (flags["on"] === undefined && flags["off"] === undefined) {
58
+ throw new Error("snapshot sticky requires exactly one of --on or --off");
59
+ }
60
+ const stickyVal = flags["on"] !== undefined ? 1 : 0;
61
+ return client.call("snapshot/toggleSticky", {
62
+ snapshot: token,
63
+ sticky: stickyVal,
64
+ });
65
+ }
66
+
67
+ export async function exportSnapshot(
68
+ args: string[],
69
+ _flags: Record<string, string>,
70
+ client: KiwiVMClient,
71
+ ): Promise<unknown> {
72
+ const token = args[0];
73
+ if (!token) {
74
+ throw new Error("snapshot export requires a <token> argument");
75
+ }
76
+ return client.call("snapshot/export", { snapshot: token });
77
+ }
78
+
79
+ export async function importSnapshot(
80
+ args: string[],
81
+ _flags: Record<string, string>,
82
+ client: KiwiVMClient,
83
+ ): Promise<unknown> {
84
+ const sourceVeid = args[0];
85
+ const sourceToken = args[1];
86
+ if (!sourceVeid || !sourceToken) {
87
+ throw new Error(
88
+ "snapshot import requires both <sourceVeid> and <sourceToken> arguments",
89
+ );
39
90
  }
91
+ return client.call("snapshot/import", { sourceVeid, sourceToken });
40
92
  }
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { KiwiVMClient } from "../client.ts";
3
+ import { audit, rateLimit, usage } from "./stats.ts";
4
+
5
+ function mockClient() {
6
+ const call = vi.fn();
7
+ return { client: { call } as unknown as KiwiVMClient, call };
8
+ }
9
+
10
+ describe("stats handlers", () => {
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 usage([], {}, client);
20
+
21
+ expect(call).toHaveBeenCalledExactlyOnceWith("getRawUsageStats");
22
+ expect(result).toMatchObject({
23
+ data: [{ month: "2025-01", usage: 100 }],
24
+ });
25
+ });
26
+
27
+ it("propagates errors from the client", async () => {
28
+ const { client, call } = mockClient();
29
+ call.mockRejectedValueOnce(new Error("API failure"));
30
+
31
+ await expect(usage([], {}, client)).rejects.toThrow("API failure");
32
+ });
33
+ });
34
+
35
+ describe("audit", () => {
36
+ it("calls getAuditLog", async () => {
37
+ const { client, call } = mockClient();
38
+ call.mockResolvedValueOnce({
39
+ error: 0,
40
+ logs: [{ timestamp: 1700000000, action: "restart" }],
41
+ });
42
+
43
+ const result = await audit([], {}, client);
44
+
45
+ expect(call).toHaveBeenCalledExactlyOnceWith("getAuditLog");
46
+ expect(result).toMatchObject({
47
+ logs: [{ timestamp: 1700000000, action: "restart" }],
48
+ });
49
+ });
50
+
51
+ it("propagates errors from the client", async () => {
52
+ const { client, call } = mockClient();
53
+ call.mockRejectedValueOnce(new Error("API failure"));
54
+
55
+ await expect(audit([], {}, client)).rejects.toThrow("API failure");
56
+ });
57
+ });
58
+
59
+ describe("rateLimit", () => {
60
+ it("calls getRateLimitStatus", async () => {
61
+ const { client, call } = mockClient();
62
+ call.mockResolvedValueOnce({
63
+ error: 0,
64
+ remaining: 950,
65
+ limit: 1000,
66
+ });
67
+
68
+ const result = await rateLimit([], {}, client);
69
+
70
+ expect(call).toHaveBeenCalledExactlyOnceWith("getRateLimitStatus");
71
+ expect(result).toMatchObject({ remaining: 950, limit: 1000 });
72
+ });
73
+
74
+ it("propagates errors from the client", async () => {
75
+ const { client, call } = mockClient();
76
+ call.mockRejectedValueOnce(new Error("Rate limited"));
77
+
78
+ await expect(rateLimit([], {}, client)).rejects.toThrow("Rate limited");
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,25 @@
1
+ import type { KiwiVMClient } from "../client.ts";
2
+
3
+ export async function usage(
4
+ _args: string[],
5
+ _flags: Record<string, string>,
6
+ client: KiwiVMClient,
7
+ ): Promise<unknown> {
8
+ return client.call("getRawUsageStats");
9
+ }
10
+
11
+ export async function audit(
12
+ _args: string[],
13
+ _flags: Record<string, string>,
14
+ client: KiwiVMClient,
15
+ ): Promise<unknown> {
16
+ return client.call("getAuditLog");
17
+ }
18
+
19
+ export async function rateLimit(
20
+ _args: string[],
21
+ _flags: Record<string, string>,
22
+ client: KiwiVMClient,
23
+ ): Promise<unknown> {
24
+ return client.call("getRateLimitStatus");
25
+ }
@@ -1,37 +1,45 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import type { KiwiVMClient } from "../client.ts";
3
- import { run } from "./system.ts";
3
+ import {
4
+ hostname,
5
+ osList,
6
+ osReinstall,
7
+ password,
8
+ sshKeySet,
9
+ sshKeyShow,
10
+ } from "./system.ts";
4
11
 
5
12
  function mockClient() {
6
13
  const call = vi.fn();
7
14
  return { client: { call } as unknown as KiwiVMClient, call };
8
15
  }
9
16
 
10
- describe("system command", () => {
17
+ describe("system handlers", () => {
11
18
  describe("hostname", () => {
12
- it("calls setHostname with new-hostname flag", async () => {
19
+ it("calls setHostname with newHostname from args", async () => {
13
20
  const { client, call } = mockClient();
14
21
  call.mockResolvedValueOnce({ error: 0 });
15
22
 
16
- await run("hostname", { newHostname: "my-vps.example.com" }, client);
23
+ await hostname(["my-vps"], {}, client);
17
24
 
18
- expect(client.call).toHaveBeenCalledExactlyOnceWith("setHostname", {
19
- newHostname: "my-vps.example.com",
25
+ expect(call).toHaveBeenCalledExactlyOnceWith("setHostname", {
26
+ newHostname: "my-vps",
20
27
  });
21
28
  });
22
- });
23
29
 
24
- describe("rdns", () => {
25
- it("calls setPTR with ip and ptr flags", async () => {
26
- const { client, call } = mockClient();
27
- call.mockResolvedValueOnce({ error: 0 });
30
+ it("throws when no hostname provided", async () => {
31
+ const { client } = mockClient();
28
32
 
29
- await run("rdns", { ip: "1.2.3.4", ptr: "my-vps.example.com" }, client);
33
+ await expect(hostname([], {}, client)).rejects.toThrow("hostname");
34
+ });
30
35
 
31
- expect(client.call).toHaveBeenCalledExactlyOnceWith("setPTR", {
32
- ip: "1.2.3.4",
33
- ptr: "my-vps.example.com",
34
- });
36
+ it("propagates errors from the client", async () => {
37
+ const { client, call } = mockClient();
38
+ call.mockRejectedValueOnce(new Error("Invalid hostname"));
39
+
40
+ await expect(hostname(["my-vps"], {}, client)).rejects.toThrow(
41
+ "Invalid hostname",
42
+ );
35
43
  });
36
44
  });
37
45
 
@@ -41,58 +49,119 @@ describe("system command", () => {
41
49
  const pwResponse = { error: 0, message: "Password reset successfully" };
42
50
  call.mockResolvedValueOnce(pwResponse);
43
51
 
44
- const result = await run("password", {}, client);
52
+ const result = await password([], {}, client);
45
53
 
46
- expect(client.call).toHaveBeenCalledExactlyOnceWith("resetRootPassword");
54
+ expect(call).toHaveBeenCalledExactlyOnceWith("resetRootPassword");
47
55
  expect(result).toEqual(pwResponse);
48
56
  });
57
+
58
+ it("propagates errors from the client", async () => {
59
+ const { client, call } = mockClient();
60
+ call.mockRejectedValueOnce(new Error("API failure"));
61
+
62
+ await expect(password([], {}, client)).rejects.toThrow("API failure");
63
+ });
49
64
  });
50
65
 
51
- describe("sshkey", () => {
52
- it("calls getSshKeys when --ssh-keys flag is not provided", async () => {
66
+ describe("osList", () => {
67
+ it("calls getAvailableOS", async () => {
53
68
  const { client, call } = mockClient();
54
69
  call.mockResolvedValueOnce({
55
70
  error: 0,
56
- sshKeys: ["ssh-rsa AAA..."],
71
+ templates: ["ubuntu-22.04", "debian-12"],
57
72
  });
58
73
 
59
- const result = await run("sshkey", {}, client);
74
+ const result = await osList([], {}, client);
60
75
 
61
- expect(client.call).toHaveBeenCalledExactlyOnceWith("getSshKeys");
62
- expect(result).toMatchObject({ sshKeys: ["ssh-rsa AAA..."] });
76
+ expect(call).toHaveBeenCalledExactlyOnceWith("getAvailableOS");
77
+ expect(result).toMatchObject({
78
+ templates: ["ubuntu-22.04", "debian-12"],
79
+ });
80
+ });
81
+
82
+ it("propagates errors from the client", async () => {
83
+ const { client, call } = mockClient();
84
+ call.mockRejectedValueOnce(new Error("API failure"));
85
+
86
+ await expect(osList([], {}, client)).rejects.toThrow("API failure");
63
87
  });
88
+ });
64
89
 
65
- it("calls updateSshKeys when --ssh-keys flag is provided", async () => {
90
+ describe("osReinstall", () => {
91
+ it("calls reinstallOS with os from args", async () => {
66
92
  const { client, call } = mockClient();
67
93
  call.mockResolvedValueOnce({ error: 0 });
68
94
 
69
- await run("sshkey", { sshKeys: "ssh-rsa AAA..." }, client);
95
+ await osReinstall(["ubuntu-22.04"], {}, client);
70
96
 
71
- expect(client.call).toHaveBeenCalledExactlyOnceWith("updateSshKeys", {
72
- sshKeys: "ssh-rsa AAA...",
97
+ expect(call).toHaveBeenCalledExactlyOnceWith("reinstallOS", {
98
+ os: "ubuntu-22.04",
73
99
  });
74
100
  });
101
+
102
+ it("throws when no OS provided", async () => {
103
+ const { client } = mockClient();
104
+
105
+ await expect(osReinstall([], {}, client)).rejects.toThrow("os");
106
+ });
107
+
108
+ it("propagates errors from the client", async () => {
109
+ const { client, call } = mockClient();
110
+ call.mockRejectedValueOnce(new Error("Invalid OS"));
111
+
112
+ await expect(osReinstall(["ubuntu-22.04"], {}, client)).rejects.toThrow(
113
+ "Invalid OS",
114
+ );
115
+ });
116
+ });
117
+
118
+ describe("sshKeyShow", () => {
119
+ it("calls getSshKeys", async () => {
120
+ const { client, call } = mockClient();
121
+ call.mockResolvedValueOnce({
122
+ error: 0,
123
+ sshKeys: ["ssh-rsa AAA..."],
124
+ });
125
+
126
+ const result = await sshKeyShow([], {}, client);
127
+
128
+ expect(call).toHaveBeenCalledExactlyOnceWith("getSshKeys");
129
+ expect(result).toMatchObject({ sshKeys: ["ssh-rsa AAA..."] });
130
+ });
131
+
132
+ it("propagates errors from the client", async () => {
133
+ const { client, call } = mockClient();
134
+ call.mockRejectedValueOnce(new Error("API failure"));
135
+
136
+ await expect(sshKeyShow([], {}, client)).rejects.toThrow("API failure");
137
+ });
75
138
  });
76
139
 
77
- describe("reinstall", () => {
78
- it("calls reinstallOS with os flag", async () => {
140
+ describe("sshKeySet", () => {
141
+ it("calls updateSshKeys with sshKey from args", async () => {
79
142
  const { client, call } = mockClient();
80
143
  call.mockResolvedValueOnce({ error: 0 });
81
144
 
82
- await run("reinstall", { os: "ubuntu-22.04" }, client);
145
+ await sshKeySet(["ssh-ed25519 AAAAC3..."], {}, client);
83
146
 
84
- expect(client.call).toHaveBeenCalledExactlyOnceWith("reinstallOS", {
85
- os: "ubuntu-22.04",
147
+ expect(call).toHaveBeenCalledExactlyOnceWith("updateSshKeys", {
148
+ ssh_keys: "ssh-ed25519 AAAAC3...",
86
149
  });
87
150
  });
88
- });
89
151
 
90
- it("propagates errors from the client", async () => {
91
- const { client, call } = mockClient();
92
- call.mockRejectedValueOnce(new Error("Invalid hostname"));
152
+ it("throws when no SSH key provided", async () => {
153
+ const { client } = mockClient();
154
+
155
+ await expect(sshKeySet([], {}, client)).rejects.toThrow(/keys/);
156
+ });
157
+
158
+ it("propagates errors from the client", async () => {
159
+ const { client, call } = mockClient();
160
+ call.mockRejectedValueOnce(new Error("API failure"));
93
161
 
94
- await expect(run("hostname", { newHostname: "" }, client)).rejects.toThrow(
95
- "Invalid hostname",
96
- );
162
+ await expect(
163
+ sshKeySet(["ssh-ed25519 AAAAC3..."], {}, client),
164
+ ).rejects.toThrow("API failure");
165
+ });
97
166
  });
98
167
  });
@@ -1,29 +1,61 @@
1
1
  import type { KiwiVMClient } from "../client.ts";
2
2
 
3
- export async function run(
4
- action: string,
5
- flags: Record<string, string>,
3
+ export async function hostname(
4
+ args: string[],
5
+ _flags: Record<string, string>,
6
6
  client: KiwiVMClient,
7
7
  ): Promise<unknown> {
8
- switch (action) {
9
- case "hostname":
10
- return client.call("setHostname", { newHostname: flags["newHostname"] });
11
- case "rdns":
12
- return client.call("setPTR", { ip: flags["ip"], ptr: flags["ptr"] });
13
- case "password":
14
- return client.call("resetRootPassword");
15
- case "sshkey":
16
- if (flags["sshKeys"] !== undefined) {
17
- return client.call("updateSshKeys", { sshKeys: flags["sshKeys"] });
18
- }
19
- return client.call("getSshKeys");
20
- case "os":
21
- return client.call("getAvailableOS");
22
- case "reinstall":
23
- return client.call("reinstallOS", { os: flags["os"] });
24
- default:
25
- throw new Error(
26
- `Unknown system action: ${action}. Valid: hostname, rdns, password, sshkey, os, reinstall`,
27
- );
8
+ const name = args[0];
9
+ if (!name) {
10
+ throw new Error("hostname requires a <name> argument");
28
11
  }
12
+ return client.call("setHostname", { newHostname: name });
13
+ }
14
+
15
+ export async function password(
16
+ _args: string[],
17
+ _flags: Record<string, string>,
18
+ client: KiwiVMClient,
19
+ ): Promise<unknown> {
20
+ return client.call("resetRootPassword");
21
+ }
22
+
23
+ export async function osList(
24
+ _args: string[],
25
+ _flags: Record<string, string>,
26
+ client: KiwiVMClient,
27
+ ): Promise<unknown> {
28
+ return client.call("getAvailableOS");
29
+ }
30
+
31
+ export async function osReinstall(
32
+ args: string[],
33
+ _flags: Record<string, string>,
34
+ client: KiwiVMClient,
35
+ ): Promise<unknown> {
36
+ const os = args[0];
37
+ if (!os) {
38
+ throw new Error("os reinstall requires a <template> argument");
39
+ }
40
+ return client.call("reinstallOS", { os });
41
+ }
42
+
43
+ export async function sshKeyShow(
44
+ _args: string[],
45
+ _flags: Record<string, string>,
46
+ client: KiwiVMClient,
47
+ ): Promise<unknown> {
48
+ return client.call("getSshKeys");
49
+ }
50
+
51
+ export async function sshKeySet(
52
+ args: string[],
53
+ _flags: Record<string, string>,
54
+ client: KiwiVMClient,
55
+ ): Promise<unknown> {
56
+ const keys = args[0];
57
+ if (!keys) {
58
+ throw new Error("ssh-key set requires a <keys> argument");
59
+ }
60
+ return client.call("updateSshKeys", { ssh_keys: keys });
29
61
  }