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,375 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// All mock variables must be created inside vi.hoisted() so they are
|
|
4
|
+
// initialized before the hoisted vi.mock() factories reference them.
|
|
5
|
+
const {
|
|
6
|
+
mockPowerRun,
|
|
7
|
+
mockInfoRun,
|
|
8
|
+
mockSnapshotRun,
|
|
9
|
+
mockBackupRun,
|
|
10
|
+
mockSystemRun,
|
|
11
|
+
mockNetworkRun,
|
|
12
|
+
mockMonitoringRun,
|
|
13
|
+
mockAdminRun,
|
|
14
|
+
mockClientConstructor,
|
|
15
|
+
} = vi.hoisted(() => ({
|
|
16
|
+
mockPowerRun: vi.fn(),
|
|
17
|
+
mockInfoRun: vi.fn(),
|
|
18
|
+
mockSnapshotRun: vi.fn(),
|
|
19
|
+
mockBackupRun: vi.fn(),
|
|
20
|
+
mockSystemRun: vi.fn(),
|
|
21
|
+
mockNetworkRun: vi.fn(),
|
|
22
|
+
mockMonitoringRun: vi.fn(),
|
|
23
|
+
mockAdminRun: vi.fn(),
|
|
24
|
+
mockClientConstructor: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("./commands/power.ts", () => ({ run: mockPowerRun }));
|
|
28
|
+
vi.mock("./commands/info.ts", () => ({ run: mockInfoRun }));
|
|
29
|
+
vi.mock("./commands/snapshot.ts", () => ({ run: mockSnapshotRun }));
|
|
30
|
+
vi.mock("./commands/backup.ts", () => ({ run: mockBackupRun }));
|
|
31
|
+
vi.mock("./commands/system.ts", () => ({ run: mockSystemRun }));
|
|
32
|
+
vi.mock("./commands/network.ts", () => ({ run: mockNetworkRun }));
|
|
33
|
+
vi.mock("./commands/monitoring.ts", () => ({ run: mockMonitoringRun }));
|
|
34
|
+
vi.mock("./commands/admin.ts", () => ({ run: mockAdminRun }));
|
|
35
|
+
vi.mock("./client.ts", () => ({
|
|
36
|
+
KiwiVMClient: mockClientConstructor,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
import { main } from "./index.ts";
|
|
40
|
+
|
|
41
|
+
function setArgv(...args: string[]) {
|
|
42
|
+
// argv[0] is node, argv[1] is script path, the rest are user args
|
|
43
|
+
vi.stubGlobal("process", {
|
|
44
|
+
...process,
|
|
45
|
+
argv: ["/usr/bin/node", "/usr/bin/kiwivm-cli", ...args],
|
|
46
|
+
exit: vi.fn() as unknown as typeof process.exit,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("CLI dispatcher", () => {
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
vi.unstubAllGlobals();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---- Dispatch to correct command handler ----
|
|
57
|
+
|
|
58
|
+
it("dispatches 'power start' to power.run", async () => {
|
|
59
|
+
setArgv("power", "start");
|
|
60
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0 });
|
|
61
|
+
|
|
62
|
+
await main();
|
|
63
|
+
|
|
64
|
+
expect(mockPowerRun).toHaveBeenCalledExactlyOnceWith(
|
|
65
|
+
"start",
|
|
66
|
+
{},
|
|
67
|
+
expect.anything(),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("dispatches 'info live' to info.run", async () => {
|
|
72
|
+
setArgv("info", "live");
|
|
73
|
+
mockInfoRun.mockResolvedValueOnce({ error: 0 });
|
|
74
|
+
|
|
75
|
+
await main();
|
|
76
|
+
|
|
77
|
+
expect(mockInfoRun).toHaveBeenCalledExactlyOnceWith(
|
|
78
|
+
"live",
|
|
79
|
+
{},
|
|
80
|
+
expect.anything(),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("dispatches 'snapshot list' to snapshot.run", async () => {
|
|
85
|
+
setArgv("snapshot", "list");
|
|
86
|
+
mockSnapshotRun.mockResolvedValueOnce({ error: 0 });
|
|
87
|
+
|
|
88
|
+
await main();
|
|
89
|
+
|
|
90
|
+
expect(mockSnapshotRun).toHaveBeenCalledExactlyOnceWith(
|
|
91
|
+
"list",
|
|
92
|
+
{},
|
|
93
|
+
expect.anything(),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("dispatches 'backup list' to backup.run", async () => {
|
|
98
|
+
setArgv("backup", "list");
|
|
99
|
+
mockBackupRun.mockResolvedValueOnce({ error: 0 });
|
|
100
|
+
|
|
101
|
+
await main();
|
|
102
|
+
|
|
103
|
+
expect(mockBackupRun).toHaveBeenCalledExactlyOnceWith(
|
|
104
|
+
"list",
|
|
105
|
+
{},
|
|
106
|
+
expect.anything(),
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("dispatches 'system password' to system.run", async () => {
|
|
111
|
+
setArgv("system", "password");
|
|
112
|
+
mockSystemRun.mockResolvedValueOnce({ error: 0 });
|
|
113
|
+
|
|
114
|
+
await main();
|
|
115
|
+
|
|
116
|
+
expect(mockSystemRun).toHaveBeenCalledExactlyOnceWith(
|
|
117
|
+
"password",
|
|
118
|
+
{},
|
|
119
|
+
expect.anything(),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("dispatches 'network ipv6-add' to network.run", async () => {
|
|
124
|
+
setArgv("network", "ipv6-add");
|
|
125
|
+
mockNetworkRun.mockResolvedValueOnce({ error: 0 });
|
|
126
|
+
|
|
127
|
+
await main();
|
|
128
|
+
|
|
129
|
+
expect(mockNetworkRun).toHaveBeenCalledExactlyOnceWith(
|
|
130
|
+
"ipv6-add",
|
|
131
|
+
{},
|
|
132
|
+
expect.anything(),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("dispatches 'monitoring audit' to monitoring.run", async () => {
|
|
137
|
+
setArgv("monitoring", "audit");
|
|
138
|
+
mockMonitoringRun.mockResolvedValueOnce({ error: 0 });
|
|
139
|
+
|
|
140
|
+
await main();
|
|
141
|
+
|
|
142
|
+
expect(mockMonitoringRun).toHaveBeenCalledExactlyOnceWith(
|
|
143
|
+
"audit",
|
|
144
|
+
{},
|
|
145
|
+
expect.anything(),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("dispatches 'admin suspensions' to admin.run", async () => {
|
|
150
|
+
setArgv("admin", "suspensions");
|
|
151
|
+
mockAdminRun.mockResolvedValueOnce({ error: 0 });
|
|
152
|
+
|
|
153
|
+
await main();
|
|
154
|
+
|
|
155
|
+
expect(mockAdminRun).toHaveBeenCalledExactlyOnceWith(
|
|
156
|
+
"suspensions",
|
|
157
|
+
{},
|
|
158
|
+
expect.anything(),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---- Flag parsing ----
|
|
163
|
+
|
|
164
|
+
it("parses --key=value flags and passes them to the handler", async () => {
|
|
165
|
+
setArgv("system", "hostname", "--newHostname=my-vps.example.com");
|
|
166
|
+
mockSystemRun.mockResolvedValueOnce({ error: 0 });
|
|
167
|
+
|
|
168
|
+
await main();
|
|
169
|
+
|
|
170
|
+
expect(mockSystemRun).toHaveBeenCalledExactlyOnceWith(
|
|
171
|
+
"hostname",
|
|
172
|
+
{ newHostname: "my-vps.example.com" },
|
|
173
|
+
expect.anything(),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("parses multiple --key=value flags", async () => {
|
|
178
|
+
setArgv("snapshot", "sticky", "--snapshot=snap1", "--sticky=1");
|
|
179
|
+
mockSnapshotRun.mockResolvedValueOnce({ error: 0 });
|
|
180
|
+
|
|
181
|
+
await main();
|
|
182
|
+
|
|
183
|
+
expect(mockSnapshotRun).toHaveBeenCalledExactlyOnceWith(
|
|
184
|
+
"sticky",
|
|
185
|
+
{ snapshot: "snap1", sticky: "1" },
|
|
186
|
+
expect.anything(),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("ignores unknown positional args beyond category and action", async () => {
|
|
191
|
+
setArgv("power", "start", "extra-arg");
|
|
192
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0 });
|
|
193
|
+
|
|
194
|
+
await main();
|
|
195
|
+
|
|
196
|
+
expect(mockPowerRun).toHaveBeenCalledExactlyOnceWith(
|
|
197
|
+
"start",
|
|
198
|
+
{},
|
|
199
|
+
expect.anything(),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("passes an empty flags object when no flags are provided", async () => {
|
|
204
|
+
setArgv("monitoring", "rate-limit");
|
|
205
|
+
mockMonitoringRun.mockResolvedValueOnce({ error: 0 });
|
|
206
|
+
|
|
207
|
+
await main();
|
|
208
|
+
|
|
209
|
+
expect(mockMonitoringRun).toHaveBeenCalledExactlyOnceWith(
|
|
210
|
+
"rate-limit",
|
|
211
|
+
{},
|
|
212
|
+
expect.anything(),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---- Auth: --veid and --api-key flags ----
|
|
217
|
+
|
|
218
|
+
it("creates KiwiVMClient with --veid and --api-key flag values", async () => {
|
|
219
|
+
setArgv("power", "start", "--veid=12345", "--api-key=secret");
|
|
220
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0 });
|
|
221
|
+
|
|
222
|
+
await main();
|
|
223
|
+
|
|
224
|
+
expect(mockClientConstructor).toHaveBeenCalledExactlyOnceWith({
|
|
225
|
+
veid: "12345",
|
|
226
|
+
apiKey: "secret",
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("strips --veid and --api-key from flags passed to handler", async () => {
|
|
231
|
+
setArgv(
|
|
232
|
+
"power",
|
|
233
|
+
"start",
|
|
234
|
+
"--veid=12345",
|
|
235
|
+
"--api-key=secret",
|
|
236
|
+
"--other=value",
|
|
237
|
+
);
|
|
238
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0 });
|
|
239
|
+
|
|
240
|
+
await main();
|
|
241
|
+
|
|
242
|
+
expect(mockPowerRun).toHaveBeenCalledExactlyOnceWith(
|
|
243
|
+
"start",
|
|
244
|
+
{ other: "value" },
|
|
245
|
+
expect.anything(),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ---- Auth: environment variable fallback ----
|
|
250
|
+
|
|
251
|
+
it("creates KiwiVMClient from KIWIVM_VEID and KIWIVM_API_KEY env vars", async () => {
|
|
252
|
+
setArgv("power", "start");
|
|
253
|
+
vi.stubEnv("KIWIVM_VEID", "env-veid");
|
|
254
|
+
vi.stubEnv("KIWIVM_API_KEY", "env-key");
|
|
255
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0 });
|
|
256
|
+
|
|
257
|
+
await main();
|
|
258
|
+
|
|
259
|
+
expect(mockClientConstructor).toHaveBeenCalledExactlyOnceWith({
|
|
260
|
+
veid: "env-veid",
|
|
261
|
+
apiKey: "env-key",
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("prefers --veid and --api-key flags over env vars", async () => {
|
|
266
|
+
setArgv("power", "start", "--veid=flag-veid", "--api-key=flag-key");
|
|
267
|
+
vi.stubEnv("KIWIVM_VEID", "env-veid");
|
|
268
|
+
vi.stubEnv("KIWIVM_API_KEY", "env-key");
|
|
269
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0 });
|
|
270
|
+
|
|
271
|
+
await main();
|
|
272
|
+
|
|
273
|
+
expect(mockClientConstructor).toHaveBeenCalledExactlyOnceWith({
|
|
274
|
+
veid: "flag-veid",
|
|
275
|
+
apiKey: "flag-key",
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---- Output: JSON to stdout ----
|
|
280
|
+
|
|
281
|
+
it("writes JSON result to stdout", async () => {
|
|
282
|
+
setArgv("power", "start");
|
|
283
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
284
|
+
mockPowerRun.mockResolvedValueOnce({ error: 0, message: "Started" });
|
|
285
|
+
|
|
286
|
+
await main();
|
|
287
|
+
|
|
288
|
+
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
|
|
289
|
+
JSON.stringify({ error: 0, message: "Started" }),
|
|
290
|
+
);
|
|
291
|
+
consoleSpy.mockRestore();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("writes pretty-printed JSON to stdout", async () => {
|
|
295
|
+
setArgv("power", "start");
|
|
296
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
297
|
+
const result = {
|
|
298
|
+
error: 0,
|
|
299
|
+
data: { key: "value", nested: { deep: true } },
|
|
300
|
+
};
|
|
301
|
+
mockPowerRun.mockResolvedValueOnce(result);
|
|
302
|
+
|
|
303
|
+
await main();
|
|
304
|
+
|
|
305
|
+
const logged = consoleSpy.mock.calls[0]?.[0] as string;
|
|
306
|
+
expect(() => JSON.parse(logged)).not.toThrow();
|
|
307
|
+
expect(JSON.parse(logged)).toEqual(result);
|
|
308
|
+
consoleSpy.mockRestore();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ---- Error handling: stderr + exit code 1 ----
|
|
312
|
+
|
|
313
|
+
it("writes errors to stderr and exits with code 1", async () => {
|
|
314
|
+
setArgv("power", "start");
|
|
315
|
+
const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
316
|
+
mockPowerRun.mockRejectedValueOnce(new Error("API failure"));
|
|
317
|
+
|
|
318
|
+
await main();
|
|
319
|
+
|
|
320
|
+
expect(stderrSpy).toHaveBeenCalledWith(
|
|
321
|
+
expect.stringContaining("API failure"),
|
|
322
|
+
);
|
|
323
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
324
|
+
stderrSpy.mockRestore();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("exits with code 1 on unknown category", async () => {
|
|
328
|
+
setArgv("unknown", "action");
|
|
329
|
+
const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
330
|
+
|
|
331
|
+
await main();
|
|
332
|
+
|
|
333
|
+
expect(stderrSpy).toHaveBeenCalled();
|
|
334
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
335
|
+
stderrSpy.mockRestore();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("exits with code 1 when neither flags nor env vars provide credentials", async () => {
|
|
339
|
+
setArgv("power", "start");
|
|
340
|
+
vi.stubEnv("KIWIVM_VEID", undefined);
|
|
341
|
+
vi.stubEnv("KIWIVM_API_KEY", undefined);
|
|
342
|
+
const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
343
|
+
|
|
344
|
+
await main();
|
|
345
|
+
|
|
346
|
+
expect(stderrSpy).toHaveBeenCalled();
|
|
347
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
348
|
+
stderrSpy.mockRestore();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ---- Help command ----
|
|
352
|
+
|
|
353
|
+
it("prints help text when category is 'help'", async () => {
|
|
354
|
+
setArgv("help");
|
|
355
|
+
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
356
|
+
|
|
357
|
+
await main();
|
|
358
|
+
|
|
359
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
360
|
+
expect.stringContaining("kiwivm-cli"),
|
|
361
|
+
);
|
|
362
|
+
consoleLogSpy.mockRestore();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("prints help and exits successfully for 'help' command", async () => {
|
|
366
|
+
setArgv("help");
|
|
367
|
+
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
368
|
+
|
|
369
|
+
await main();
|
|
370
|
+
|
|
371
|
+
// Help should not call process.exit with an error code
|
|
372
|
+
expect(process.exit).not.toHaveBeenCalledWith(1);
|
|
373
|
+
consoleLogSpy.mockRestore();
|
|
374
|
+
});
|
|
375
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { KiwiVMClient } from "./client.ts";
|
|
4
|
+
import { KiwiVMError } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
function parseFlags(args: string[]): Record<string, string> {
|
|
7
|
+
const flags: Record<string, string> = {};
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const arg = args[i];
|
|
10
|
+
if (!arg?.startsWith("--")) continue;
|
|
11
|
+
const eqIdx = arg.indexOf("=");
|
|
12
|
+
if (eqIdx !== -1) {
|
|
13
|
+
const key = arg.slice(2, eqIdx);
|
|
14
|
+
const value = arg.slice(eqIdx + 1);
|
|
15
|
+
flags[toCamelCase(key)] = value;
|
|
16
|
+
} else {
|
|
17
|
+
const next = args[i + 1];
|
|
18
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
19
|
+
const key = arg.slice(2);
|
|
20
|
+
flags[toCamelCase(key)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
} else {
|
|
23
|
+
// --flag with no value (treat as boolean/empty string)
|
|
24
|
+
const key = arg.slice(2);
|
|
25
|
+
flags[toCamelCase(key)] = "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return flags;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toCamelCase(key: string): string {
|
|
33
|
+
// Known mappings: kebab-case flags that need camelCase API param names
|
|
34
|
+
const known: Record<string, string> = {
|
|
35
|
+
"backup-token": "backupToken",
|
|
36
|
+
"new-hostname": "newHostname",
|
|
37
|
+
"ssh-keys": "sshKeys",
|
|
38
|
+
"source-veid": "sourceVeid",
|
|
39
|
+
"source-token": "sourceToken",
|
|
40
|
+
"record-id": "recordId",
|
|
41
|
+
"api-key": "apiKey",
|
|
42
|
+
"rate-limit": "rateLimit",
|
|
43
|
+
};
|
|
44
|
+
if (known[key]) return known[key];
|
|
45
|
+
|
|
46
|
+
// Default: convert --some-flag to someFlag
|
|
47
|
+
return key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getCommandFromArgs(args: string[]): {
|
|
51
|
+
positional: string[];
|
|
52
|
+
flags: Record<string, string>;
|
|
53
|
+
} {
|
|
54
|
+
const positional: string[] = [];
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < args.length) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
if (!arg || arg.startsWith("--")) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
positional.push(arg);
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
const flagArgs = args.slice(i);
|
|
65
|
+
const flags = parseFlags(flagArgs);
|
|
66
|
+
return { positional, flags };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const args = process.argv.slice(2);
|
|
71
|
+
const { positional, flags } = getCommandFromArgs(args);
|
|
72
|
+
|
|
73
|
+
const category = positional[0] ?? "";
|
|
74
|
+
const action = positional[1] ?? "";
|
|
75
|
+
|
|
76
|
+
// Handle help before auth (no credentials needed)
|
|
77
|
+
if (category === "" || category === "help") {
|
|
78
|
+
const { run } = await import("./commands/help.ts");
|
|
79
|
+
const text = await run();
|
|
80
|
+
console.log(text as string);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve auth: flags first, then env vars
|
|
85
|
+
const flagVeid = flags["veid"];
|
|
86
|
+
const flagApiKey = flags["apiKey"];
|
|
87
|
+
const resolvedVeid = flagVeid || process.env["KIWIVM_VEID"];
|
|
88
|
+
const resolvedApiKey = flagApiKey || process.env["KIWIVM_API_KEY"];
|
|
89
|
+
|
|
90
|
+
if (!resolvedVeid || !resolvedApiKey) {
|
|
91
|
+
console.error(
|
|
92
|
+
"Error: VEID and API key are required. Use --veid and --api-key flags, or set KIWIVM_VEID and KIWIVM_API_KEY environment variables.",
|
|
93
|
+
);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Strip auth flags before passing to handlers
|
|
98
|
+
const handlerFlags = { ...flags };
|
|
99
|
+
delete handlerFlags["veid"];
|
|
100
|
+
delete handlerFlags["apiKey"];
|
|
101
|
+
|
|
102
|
+
const client = new KiwiVMClient({
|
|
103
|
+
veid: resolvedVeid,
|
|
104
|
+
apiKey: resolvedApiKey,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
let result: unknown;
|
|
109
|
+
|
|
110
|
+
switch (category) {
|
|
111
|
+
case "power": {
|
|
112
|
+
const { run } = await import("./commands/power.ts");
|
|
113
|
+
result = await run(action, handlerFlags, client);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "info": {
|
|
117
|
+
const { run } = await import("./commands/info.ts");
|
|
118
|
+
result = await run(action, handlerFlags, client);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "snapshot": {
|
|
122
|
+
const { run } = await import("./commands/snapshot.ts");
|
|
123
|
+
result = await run(action, handlerFlags, client);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "backup": {
|
|
127
|
+
const { run } = await import("./commands/backup.ts");
|
|
128
|
+
result = await run(action, handlerFlags, client);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "system": {
|
|
132
|
+
const { run } = await import("./commands/system.ts");
|
|
133
|
+
result = await run(action, handlerFlags, client);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "network": {
|
|
137
|
+
const { run } = await import("./commands/network.ts");
|
|
138
|
+
result = await run(action, handlerFlags, client);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "monitoring": {
|
|
142
|
+
const { run } = await import("./commands/monitoring.ts");
|
|
143
|
+
result = await run(action, handlerFlags, client);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "admin": {
|
|
147
|
+
const { run } = await import("./commands/admin.ts");
|
|
148
|
+
result = await run(action, handlerFlags, client);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
default: {
|
|
152
|
+
console.error(`Unknown category: ${category}`);
|
|
153
|
+
console.error("Run 'kiwivm-cli help' for usage.");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(JSON.stringify(result));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message =
|
|
161
|
+
error instanceof KiwiVMError ? error.message : String(error);
|
|
162
|
+
console.error(`Error: ${message}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { main };
|
|
168
|
+
|
|
169
|
+
// Only auto-run when executed directly (not when imported by tests)
|
|
170
|
+
if (!process.env["VITEST"]) {
|
|
171
|
+
main();
|
|
172
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface KiwiVMResponse {
|
|
2
|
+
error: number;
|
|
3
|
+
message?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class KiwiVMError extends Error {
|
|
7
|
+
public readonly errorCode: number;
|
|
8
|
+
public readonly rawResponse: KiwiVMResponse;
|
|
9
|
+
|
|
10
|
+
constructor(message: string, errorCode: number, rawResponse: KiwiVMResponse) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "KiwiVMError";
|
|
13
|
+
this.errorCode = errorCode;
|
|
14
|
+
this.rawResponse = rawResponse;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServiceInfo extends KiwiVMResponse {
|
|
19
|
+
hostname?: string;
|
|
20
|
+
node_alias?: string;
|
|
21
|
+
node_location?: string;
|
|
22
|
+
location_ipv6_ready?: number;
|
|
23
|
+
plan?: string;
|
|
24
|
+
plan_monthly_data?: number;
|
|
25
|
+
plan_disk?: number;
|
|
26
|
+
plan_ram?: number;
|
|
27
|
+
plan_swap?: number;
|
|
28
|
+
os?: string;
|
|
29
|
+
email?: string;
|
|
30
|
+
data_counter?: number;
|
|
31
|
+
data_next_reset?: number;
|
|
32
|
+
ip_addresses?: string[];
|
|
33
|
+
private_ip_addresses?: string[];
|
|
34
|
+
ip_nullroutes?: Record<
|
|
35
|
+
string,
|
|
36
|
+
{
|
|
37
|
+
nullroute_timestamp: number;
|
|
38
|
+
nullroute_duration_s: number;
|
|
39
|
+
log: string;
|
|
40
|
+
}
|
|
41
|
+
>;
|
|
42
|
+
iso1?: string;
|
|
43
|
+
iso2?: string;
|
|
44
|
+
available_isos?: string[];
|
|
45
|
+
plan_max_ipv6s?: number;
|
|
46
|
+
rdns_api_available?: number;
|
|
47
|
+
plan_private_network_available?: number;
|
|
48
|
+
location_private_network_available?: number;
|
|
49
|
+
ptr?: Record<string, string>;
|
|
50
|
+
suspended?: number;
|
|
51
|
+
policy_violation?: number;
|
|
52
|
+
suspension_count?: number;
|
|
53
|
+
total_abuse_points?: number;
|
|
54
|
+
max_abuse_points?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LiveServiceInfo extends ServiceInfo {
|
|
58
|
+
vm_type?: "ovz" | "kvm";
|
|
59
|
+
vz_status?: Record<string, unknown>;
|
|
60
|
+
vz_quota?: Record<string, unknown>;
|
|
61
|
+
ve_status?: "Starting" | "Running" | "Stopped";
|
|
62
|
+
ve_mac1?: string;
|
|
63
|
+
ve_used_disk_space_b?: number;
|
|
64
|
+
ve_disk_quota_gb?: number;
|
|
65
|
+
is_cpu_throttled?: number;
|
|
66
|
+
is_disk_throttled?: number;
|
|
67
|
+
ssh_port?: number;
|
|
68
|
+
live_hostname?: string;
|
|
69
|
+
load_average?: string;
|
|
70
|
+
mem_available_kb?: number;
|
|
71
|
+
swap_total_kb?: number;
|
|
72
|
+
swap_available_kb?: number;
|
|
73
|
+
screendump_png_base64?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Snapshot {
|
|
77
|
+
fileName: string;
|
|
78
|
+
os: string;
|
|
79
|
+
description: string;
|
|
80
|
+
size: number;
|
|
81
|
+
md5: string;
|
|
82
|
+
sticky: number;
|
|
83
|
+
purgesIn: number;
|
|
84
|
+
downloadLink: string;
|
|
85
|
+
downloadLinkSSL: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface Backup {
|
|
89
|
+
backupToken: string;
|
|
90
|
+
size: number;
|
|
91
|
+
os: string;
|
|
92
|
+
md5: string;
|
|
93
|
+
timestamp: number;
|
|
94
|
+
}
|