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
package/src/index.test.ts CHANGED
@@ -3,35 +3,167 @@ import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  // All mock variables must be created inside vi.hoisted() so they are
4
4
  // initialized before the hoisted vi.mock() factories reference them.
5
5
  const {
6
- mockPowerRun,
7
- mockInfoRun,
8
- mockSnapshotRun,
9
- mockBackupRun,
10
- mockSystemRun,
11
- mockNetworkRun,
12
- mockMonitoringRun,
13
- mockAdminRun,
6
+ mockPowerStart,
7
+ mockPowerStop,
8
+ mockPowerRestart,
9
+ mockPowerKill,
10
+ mockInfoInfo,
11
+ mockInfoStatus,
12
+ mockSnapshotList,
13
+ mockSnapshotCreate,
14
+ mockSnapshotDelete,
15
+ mockSnapshotRestore,
16
+ mockSnapshotSticky,
17
+ mockSnapshotExport,
18
+ mockSnapshotImport,
19
+ mockBackupList,
20
+ mockBackupCopy,
21
+ mockSystemHostname,
22
+ mockSystemPassword,
23
+ mockSystemOsList,
24
+ mockSystemOsReinstall,
25
+ mockSystemSshKeyShow,
26
+ mockSystemSshKeySet,
27
+ mockNetworkRdnsSet,
28
+ mockNetworkIpv6Add,
29
+ mockNetworkIpv6Delete,
30
+ mockNetworkPrivateIpList,
31
+ mockNetworkPrivateIpAssign,
32
+ mockNetworkPrivateIpDelete,
33
+ mockIsoMount,
34
+ mockIsoUnmount,
35
+ mockShellExec,
36
+ mockShellScript,
37
+ mockMigrateLocations,
38
+ mockMigrateStart,
39
+ mockMigrateClone,
40
+ mockStatsUsage,
41
+ mockStatsAudit,
42
+ mockStatsRateLimit,
43
+ mockAdminSuspensions,
44
+ mockAdminUnsuspend,
45
+ mockAdminViolationsList,
46
+ mockAdminViolationsResolve,
47
+ mockAdminNotificationsGet,
48
+ mockAdminNotificationsSet,
49
+ mockHelpRun,
14
50
  mockClientConstructor,
15
51
  } = 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(),
52
+ mockPowerStart: vi.fn(),
53
+ mockPowerStop: vi.fn(),
54
+ mockPowerRestart: vi.fn(),
55
+ mockPowerKill: vi.fn(),
56
+ mockInfoInfo: vi.fn(),
57
+ mockInfoStatus: vi.fn(),
58
+ mockSnapshotList: vi.fn(),
59
+ mockSnapshotCreate: vi.fn(),
60
+ mockSnapshotDelete: vi.fn(),
61
+ mockSnapshotRestore: vi.fn(),
62
+ mockSnapshotSticky: vi.fn(),
63
+ mockSnapshotExport: vi.fn(),
64
+ mockSnapshotImport: vi.fn(),
65
+ mockBackupList: vi.fn(),
66
+ mockBackupCopy: vi.fn(),
67
+ mockSystemHostname: vi.fn(),
68
+ mockSystemPassword: vi.fn(),
69
+ mockSystemOsList: vi.fn(),
70
+ mockSystemOsReinstall: vi.fn(),
71
+ mockSystemSshKeyShow: vi.fn(),
72
+ mockSystemSshKeySet: vi.fn(),
73
+ mockNetworkRdnsSet: vi.fn(),
74
+ mockNetworkIpv6Add: vi.fn(),
75
+ mockNetworkIpv6Delete: vi.fn(),
76
+ mockNetworkPrivateIpList: vi.fn(),
77
+ mockNetworkPrivateIpAssign: vi.fn(),
78
+ mockNetworkPrivateIpDelete: vi.fn(),
79
+ mockIsoMount: vi.fn(),
80
+ mockIsoUnmount: vi.fn(),
81
+ mockShellExec: vi.fn(),
82
+ mockShellScript: vi.fn(),
83
+ mockMigrateLocations: vi.fn(),
84
+ mockMigrateStart: vi.fn(),
85
+ mockMigrateClone: vi.fn(),
86
+ mockStatsUsage: vi.fn(),
87
+ mockStatsAudit: vi.fn(),
88
+ mockStatsRateLimit: vi.fn(),
89
+ mockAdminSuspensions: vi.fn(),
90
+ mockAdminUnsuspend: vi.fn(),
91
+ mockAdminViolationsList: vi.fn(),
92
+ mockAdminViolationsResolve: vi.fn(),
93
+ mockAdminNotificationsGet: vi.fn(),
94
+ mockAdminNotificationsSet: vi.fn(),
95
+ mockHelpRun: vi.fn(),
24
96
  mockClientConstructor: vi.fn(),
25
97
  }));
26
98
 
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 }));
99
+ vi.mock("./commands/power.ts", () => ({
100
+ start: mockPowerStart,
101
+ stop: mockPowerStop,
102
+ restart: mockPowerRestart,
103
+ kill: mockPowerKill,
104
+ }));
105
+ vi.mock("./commands/info.ts", () => ({
106
+ info: mockInfoInfo,
107
+ status: mockInfoStatus,
108
+ }));
109
+ vi.mock("./commands/snapshot.ts", () => ({
110
+ list: mockSnapshotList,
111
+ create: mockSnapshotCreate,
112
+ deleteSnapshot: mockSnapshotDelete,
113
+ restore: mockSnapshotRestore,
114
+ sticky: mockSnapshotSticky,
115
+ exportSnapshot: mockSnapshotExport,
116
+ importSnapshot: mockSnapshotImport,
117
+ }));
118
+ vi.mock("./commands/backup.ts", () => ({
119
+ list: mockBackupList,
120
+ copy: mockBackupCopy,
121
+ }));
122
+ vi.mock("./commands/system.ts", () => ({
123
+ hostname: mockSystemHostname,
124
+ password: mockSystemPassword,
125
+ osList: mockSystemOsList,
126
+ osReinstall: mockSystemOsReinstall,
127
+ sshKeyShow: mockSystemSshKeyShow,
128
+ sshKeySet: mockSystemSshKeySet,
129
+ }));
130
+ vi.mock("./commands/network.ts", () => ({
131
+ rdnsSet: mockNetworkRdnsSet,
132
+ ipv6Add: mockNetworkIpv6Add,
133
+ ipv6Delete: mockNetworkIpv6Delete,
134
+ privateIpList: mockNetworkPrivateIpList,
135
+ privateIpAssign: mockNetworkPrivateIpAssign,
136
+ privateIpDelete: mockNetworkPrivateIpDelete,
137
+ }));
138
+ vi.mock("./commands/iso.ts", () => ({
139
+ mount: mockIsoMount,
140
+ unmount: mockIsoUnmount,
141
+ }));
142
+ vi.mock("./commands/shell.ts", () => ({
143
+ exec: mockShellExec,
144
+ script: mockShellScript,
145
+ }));
146
+ vi.mock("./commands/migrate.ts", () => ({
147
+ locations: mockMigrateLocations,
148
+ migrateStart: mockMigrateStart,
149
+ clone: mockMigrateClone,
150
+ }));
151
+ vi.mock("./commands/stats.ts", () => ({
152
+ usage: mockStatsUsage,
153
+ audit: mockStatsAudit,
154
+ rateLimit: mockStatsRateLimit,
155
+ }));
156
+ vi.mock("./commands/admin.ts", () => ({
157
+ suspensions: mockAdminSuspensions,
158
+ unsuspend: mockAdminUnsuspend,
159
+ violationsList: mockAdminViolationsList,
160
+ violationsResolve: mockAdminViolationsResolve,
161
+ notificationsGet: mockAdminNotificationsGet,
162
+ notificationsSet: mockAdminNotificationsSet,
163
+ }));
164
+ vi.mock("./commands/help.ts", () => ({
165
+ run: mockHelpRun,
166
+ }));
35
167
  vi.mock("./client.ts", () => ({
36
168
  KiwiVMClient: mockClientConstructor,
37
169
  }));
@@ -53,206 +185,354 @@ describe("CLI dispatcher", () => {
53
185
  vi.unstubAllGlobals();
54
186
  });
55
187
 
56
- // ---- Dispatch to correct command handler ----
188
+ // ---- Flat command dispatch ----
57
189
 
58
- it("dispatches 'power start' to power.run", async () => {
59
- setArgv("power", "start");
60
- mockPowerRun.mockResolvedValueOnce({ error: 0 });
190
+ it("dispatches 'start' to power.start", async () => {
191
+ setArgv("start");
192
+ mockPowerStart.mockResolvedValueOnce({ error: 0 });
61
193
 
62
194
  await main();
63
195
 
64
- expect(mockPowerRun).toHaveBeenCalledExactlyOnceWith(
65
- "start",
196
+ expect(mockPowerStart).toHaveBeenCalledExactlyOnceWith(
197
+ [],
66
198
  {},
67
199
  expect.anything(),
68
200
  );
69
201
  });
70
202
 
71
- it("dispatches 'info live' to info.run", async () => {
72
- setArgv("info", "live");
73
- mockInfoRun.mockResolvedValueOnce({ error: 0 });
203
+ it("dispatches 'stop' to power.stop", async () => {
204
+ setArgv("stop");
205
+ mockPowerStop.mockResolvedValueOnce({ error: 0 });
74
206
 
75
207
  await main();
76
208
 
77
- expect(mockInfoRun).toHaveBeenCalledExactlyOnceWith(
78
- "live",
209
+ expect(mockPowerStop).toHaveBeenCalledExactlyOnceWith(
210
+ [],
79
211
  {},
80
212
  expect.anything(),
81
213
  );
82
214
  });
83
215
 
84
- it("dispatches 'snapshot list' to snapshot.run", async () => {
85
- setArgv("snapshot", "list");
86
- mockSnapshotRun.mockResolvedValueOnce({ error: 0 });
216
+ it("dispatches 'restart' to power.restart", async () => {
217
+ setArgv("restart");
218
+ mockPowerRestart.mockResolvedValueOnce({ error: 0 });
87
219
 
88
220
  await main();
89
221
 
90
- expect(mockSnapshotRun).toHaveBeenCalledExactlyOnceWith(
91
- "list",
222
+ expect(mockPowerRestart).toHaveBeenCalledExactlyOnceWith(
223
+ [],
92
224
  {},
93
225
  expect.anything(),
94
226
  );
95
227
  });
96
228
 
97
- it("dispatches 'backup list' to backup.run", async () => {
98
- setArgv("backup", "list");
99
- mockBackupRun.mockResolvedValueOnce({ error: 0 });
229
+ it("dispatches 'kill' to power.kill", async () => {
230
+ setArgv("kill");
231
+ mockPowerKill.mockResolvedValueOnce({ error: 0 });
100
232
 
101
233
  await main();
102
234
 
103
- expect(mockBackupRun).toHaveBeenCalledExactlyOnceWith(
104
- "list",
235
+ expect(mockPowerKill).toHaveBeenCalledExactlyOnceWith(
236
+ [],
105
237
  {},
106
238
  expect.anything(),
107
239
  );
108
240
  });
109
241
 
110
- it("dispatches 'system password' to system.run", async () => {
111
- setArgv("system", "password");
112
- mockSystemRun.mockResolvedValueOnce({ error: 0 });
242
+ it("dispatches 'info' to info.info", async () => {
243
+ setArgv("info");
244
+ mockInfoInfo.mockResolvedValueOnce({ error: 0 });
113
245
 
114
246
  await main();
115
247
 
116
- expect(mockSystemRun).toHaveBeenCalledExactlyOnceWith(
117
- "password",
248
+ expect(mockInfoInfo).toHaveBeenCalledExactlyOnceWith(
249
+ [],
118
250
  {},
119
251
  expect.anything(),
120
252
  );
121
253
  });
122
254
 
123
- it("dispatches 'network ipv6-add' to network.run", async () => {
124
- setArgv("network", "ipv6-add");
125
- mockNetworkRun.mockResolvedValueOnce({ error: 0 });
255
+ it("dispatches 'status' to info.status", async () => {
256
+ setArgv("status");
257
+ mockInfoStatus.mockResolvedValueOnce({ error: 0 });
126
258
 
127
259
  await main();
128
260
 
129
- expect(mockNetworkRun).toHaveBeenCalledExactlyOnceWith(
130
- "ipv6-add",
261
+ expect(mockInfoStatus).toHaveBeenCalledExactlyOnceWith(
262
+ [],
131
263
  {},
132
264
  expect.anything(),
133
265
  );
134
266
  });
135
267
 
136
- it("dispatches 'monitoring audit' to monitoring.run", async () => {
137
- setArgv("monitoring", "audit");
138
- mockMonitoringRun.mockResolvedValueOnce({ error: 0 });
268
+ it("dispatches 'hostname' with args to system.hostname", async () => {
269
+ setArgv("hostname", "my-vps");
270
+ mockSystemHostname.mockResolvedValueOnce({ error: 0 });
139
271
 
140
272
  await main();
141
273
 
142
- expect(mockMonitoringRun).toHaveBeenCalledExactlyOnceWith(
143
- "audit",
274
+ expect(mockSystemHostname).toHaveBeenCalledExactlyOnceWith(
275
+ ["my-vps"],
144
276
  {},
145
277
  expect.anything(),
146
278
  );
147
279
  });
148
280
 
149
- it("dispatches 'admin suspensions' to admin.run", async () => {
150
- setArgv("admin", "suspensions");
151
- mockAdminRun.mockResolvedValueOnce({ error: 0 });
281
+ it("dispatches 'password' to system.password", async () => {
282
+ setArgv("password");
283
+ mockSystemPassword.mockResolvedValueOnce({ error: 0 });
152
284
 
153
285
  await main();
154
286
 
155
- expect(mockAdminRun).toHaveBeenCalledExactlyOnceWith(
156
- "suspensions",
287
+ expect(mockSystemPassword).toHaveBeenCalledExactlyOnceWith(
288
+ [],
157
289
  {},
158
290
  expect.anything(),
159
291
  );
160
292
  });
161
293
 
162
- // ---- Flag parsing ----
294
+ it("dispatches 'suspensions' to admin.suspensions", async () => {
295
+ setArgv("suspensions");
296
+ mockAdminSuspensions.mockResolvedValueOnce({ error: 0 });
297
+
298
+ await main();
299
+
300
+ expect(mockAdminSuspensions).toHaveBeenCalledExactlyOnceWith(
301
+ [],
302
+ {},
303
+ expect.anything(),
304
+ );
305
+ });
163
306
 
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 });
307
+ it("dispatches 'unsuspend' with args to admin.unsuspend", async () => {
308
+ setArgv("unsuspend", "42");
309
+ mockAdminUnsuspend.mockResolvedValueOnce({ error: 0 });
167
310
 
168
311
  await main();
169
312
 
170
- expect(mockSystemRun).toHaveBeenCalledExactlyOnceWith(
171
- "hostname",
172
- { newHostname: "my-vps.example.com" },
313
+ expect(mockAdminUnsuspend).toHaveBeenCalledExactlyOnceWith(
314
+ ["42"],
315
+ {},
173
316
  expect.anything(),
174
317
  );
175
318
  });
176
319
 
177
- it("parses multiple --key=value flags", async () => {
178
- setArgv("snapshot", "sticky", "--snapshot=snap1", "--sticky=1");
179
- mockSnapshotRun.mockResolvedValueOnce({ error: 0 });
320
+ it("dispatches 'clone' with args to migrate.clone", async () => {
321
+ setArgv("clone", "1.2.3.4", "pass");
322
+ mockMigrateClone.mockResolvedValueOnce({ error: 0 });
180
323
 
181
324
  await main();
182
325
 
183
- expect(mockSnapshotRun).toHaveBeenCalledExactlyOnceWith(
184
- "sticky",
185
- { snapshot: "snap1", sticky: "1" },
326
+ expect(mockMigrateClone).toHaveBeenCalledExactlyOnceWith(
327
+ ["1.2.3.4", "pass"],
328
+ {},
186
329
  expect.anything(),
187
330
  );
188
331
  });
189
332
 
190
- it("ignores unknown positional args beyond category and action", async () => {
191
- setArgv("power", "start", "extra-arg");
192
- mockPowerRun.mockResolvedValueOnce({ error: 0 });
333
+ // ---- Subcommand dispatch ----
334
+
335
+ it("dispatches 'snapshot list' to snapshot.list", async () => {
336
+ setArgv("snapshot", "list");
337
+ mockSnapshotList.mockResolvedValueOnce({ error: 0 });
193
338
 
194
339
  await main();
195
340
 
196
- expect(mockPowerRun).toHaveBeenCalledExactlyOnceWith(
197
- "start",
341
+ expect(mockSnapshotList).toHaveBeenCalledExactlyOnceWith(
342
+ [],
198
343
  {},
199
344
  expect.anything(),
200
345
  );
201
346
  });
202
347
 
203
- it("passes an empty flags object when no flags are provided", async () => {
204
- setArgv("monitoring", "rate-limit");
205
- mockMonitoringRun.mockResolvedValueOnce({ error: 0 });
348
+ it("dispatches 'snapshot delete' with args to snapshot.deleteSnapshot", async () => {
349
+ setArgv("snapshot", "delete", "vsb123");
350
+ mockSnapshotDelete.mockResolvedValueOnce({ error: 0 });
206
351
 
207
352
  await main();
208
353
 
209
- expect(mockMonitoringRun).toHaveBeenCalledExactlyOnceWith(
210
- "rate-limit",
354
+ expect(mockSnapshotDelete).toHaveBeenCalledExactlyOnceWith(
355
+ ["vsb123"],
211
356
  {},
212
357
  expect.anything(),
213
358
  );
214
359
  });
215
360
 
216
- // ---- Auth: --veid and --api-key flags ----
361
+ it("dispatches 'snapshot create' with flags to snapshot.create", async () => {
362
+ setArgv("snapshot", "create", "--desc=test");
363
+ mockSnapshotCreate.mockResolvedValueOnce({ error: 0 });
217
364
 
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 });
365
+ await main();
366
+
367
+ expect(mockSnapshotCreate).toHaveBeenCalledExactlyOnceWith(
368
+ [],
369
+ { desc: "test" },
370
+ expect.anything(),
371
+ );
372
+ });
373
+
374
+ it("dispatches 'snapshot sticky' with --on flag", async () => {
375
+ setArgv("snapshot", "sticky", "snap1", "--on");
376
+ mockSnapshotSticky.mockResolvedValueOnce({ error: 0 });
221
377
 
222
378
  await main();
223
379
 
224
- expect(mockClientConstructor).toHaveBeenCalledExactlyOnceWith({
225
- veid: "12345",
226
- apiKey: "secret",
227
- });
380
+ expect(mockSnapshotSticky).toHaveBeenCalledExactlyOnceWith(
381
+ ["snap1"],
382
+ { on: "1" },
383
+ expect.anything(),
384
+ );
228
385
  });
229
386
 
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",
387
+ it("dispatches 'backup list' to backup.list", async () => {
388
+ setArgv("backup", "list");
389
+ mockBackupList.mockResolvedValueOnce({ error: 0 });
390
+
391
+ await main();
392
+
393
+ expect(mockBackupList).toHaveBeenCalledExactlyOnceWith(
394
+ [],
395
+ {},
396
+ expect.anything(),
237
397
  );
238
- mockPowerRun.mockResolvedValueOnce({ error: 0 });
398
+ });
399
+
400
+ it("dispatches 'backup copy' with args to backup.copy", async () => {
401
+ setArgv("backup", "copy", "tok123");
402
+ mockBackupCopy.mockResolvedValueOnce({ error: 0 });
239
403
 
240
404
  await main();
241
405
 
242
- expect(mockPowerRun).toHaveBeenCalledExactlyOnceWith(
243
- "start",
244
- { other: "value" },
406
+ expect(mockBackupCopy).toHaveBeenCalledExactlyOnceWith(
407
+ ["tok123"],
408
+ {},
409
+ expect.anything(),
410
+ );
411
+ });
412
+
413
+ // ---- Default subcommand (no action specified) ----
414
+
415
+ it("dispatches 'ssh-key' (no action) as default to system.sshKeyShow", async () => {
416
+ setArgv("ssh-key");
417
+ mockSystemSshKeyShow.mockResolvedValueOnce({ error: 0 });
418
+
419
+ await main();
420
+
421
+ expect(mockSystemSshKeyShow).toHaveBeenCalledExactlyOnceWith(
422
+ [],
423
+ {},
424
+ expect.anything(),
425
+ );
426
+ });
427
+
428
+ it("dispatches 'ssh-key set' with args to system.sshKeySet", async () => {
429
+ setArgv("ssh-key", "set", "ssh-ed25519 AAAAC3...");
430
+ mockSystemSshKeySet.mockResolvedValueOnce({ error: 0 });
431
+
432
+ await main();
433
+
434
+ expect(mockSystemSshKeySet).toHaveBeenCalledExactlyOnceWith(
435
+ ["ssh-ed25519 AAAAC3..."],
436
+ {},
437
+ expect.anything(),
438
+ );
439
+ });
440
+
441
+ it("dispatches 'violations' (no action) as default to admin.violationsList", async () => {
442
+ setArgv("violations");
443
+ mockAdminViolationsList.mockResolvedValueOnce({ error: 0 });
444
+
445
+ await main();
446
+
447
+ expect(mockAdminViolationsList).toHaveBeenCalledExactlyOnceWith(
448
+ [],
449
+ {},
450
+ expect.anything(),
451
+ );
452
+ });
453
+
454
+ it("dispatches 'violations resolve' with args to admin.violationsResolve", async () => {
455
+ setArgv("violations", "resolve", "14");
456
+ mockAdminViolationsResolve.mockResolvedValueOnce({ error: 0 });
457
+
458
+ await main();
459
+
460
+ expect(mockAdminViolationsResolve).toHaveBeenCalledExactlyOnceWith(
461
+ ["14"],
462
+ {},
245
463
  expect.anything(),
246
464
  );
247
465
  });
248
466
 
249
- // ---- Auth: environment variable fallback ----
467
+ it("dispatches 'notifications' (no action) as default to admin.notificationsGet", async () => {
468
+ setArgv("notifications");
469
+ mockAdminNotificationsGet.mockResolvedValueOnce({ error: 0 });
470
+
471
+ await main();
472
+
473
+ expect(mockAdminNotificationsGet).toHaveBeenCalledExactlyOnceWith(
474
+ [],
475
+ {},
476
+ expect.anything(),
477
+ );
478
+ });
479
+
480
+ it("dispatches 'notifications set' to admin.notificationsSet", async () => {
481
+ setArgv("notifications", "set", '{"1":1,"2":0}');
482
+ mockAdminNotificationsSet.mockResolvedValueOnce({ error: 0 });
483
+
484
+ await main();
485
+
486
+ expect(mockAdminNotificationsSet).toHaveBeenCalledExactlyOnceWith(
487
+ ['{"1":1,"2":0}'],
488
+ {},
489
+ expect.anything(),
490
+ );
491
+ });
492
+
493
+ // ---- Unknown command / subcommand ----
494
+
495
+ it("exits with error code 1 on unknown command", async () => {
496
+ setArgv("unknown-command");
497
+ const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
498
+
499
+ await main();
500
+
501
+ expect(stderrSpy).toHaveBeenCalled();
502
+ expect(process.exit).toHaveBeenCalledWith(1);
503
+ stderrSpy.mockRestore();
504
+ });
505
+
506
+ it("exits with error code 1 on unknown subcommand", async () => {
507
+ setArgv("snapshot", "unknown-action");
508
+ const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
509
+
510
+ await main();
511
+
512
+ expect(stderrSpy).toHaveBeenCalled();
513
+ expect(process.exit).toHaveBeenCalledWith(1);
514
+ stderrSpy.mockRestore();
515
+ });
516
+
517
+ // ---- Auth: --veid and --api-key flags ----
518
+
519
+ it("creates KiwiVMClient with --veid and --api-key flag values", async () => {
520
+ setArgv("start", "--veid=12345", "--api-key=secret");
521
+ mockPowerStart.mockResolvedValueOnce({ error: 0 });
522
+
523
+ await main();
524
+
525
+ expect(mockClientConstructor).toHaveBeenCalledExactlyOnceWith({
526
+ veid: "12345",
527
+ apiKey: "secret",
528
+ });
529
+ });
250
530
 
251
531
  it("creates KiwiVMClient from KIWIVM_VEID and KIWIVM_API_KEY env vars", async () => {
252
- setArgv("power", "start");
532
+ setArgv("start");
253
533
  vi.stubEnv("KIWIVM_VEID", "env-veid");
254
534
  vi.stubEnv("KIWIVM_API_KEY", "env-key");
255
- mockPowerRun.mockResolvedValueOnce({ error: 0 });
535
+ mockPowerStart.mockResolvedValueOnce({ error: 0 });
256
536
 
257
537
  await main();
258
538
 
@@ -263,10 +543,10 @@ describe("CLI dispatcher", () => {
263
543
  });
264
544
 
265
545
  it("prefers --veid and --api-key flags over env vars", async () => {
266
- setArgv("power", "start", "--veid=flag-veid", "--api-key=flag-key");
546
+ setArgv("start", "--veid=flag-veid", "--api-key=flag-key");
267
547
  vi.stubEnv("KIWIVM_VEID", "env-veid");
268
548
  vi.stubEnv("KIWIVM_API_KEY", "env-key");
269
- mockPowerRun.mockResolvedValueOnce({ error: 0 });
549
+ mockPowerStart.mockResolvedValueOnce({ error: 0 });
270
550
 
271
551
  await main();
272
552
 
@@ -276,44 +556,57 @@ describe("CLI dispatcher", () => {
276
556
  });
277
557
  });
278
558
 
279
- // ---- Output: JSON to stdout ----
559
+ // ---- Flag stripping ----
280
560
 
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" });
561
+ it("strips --veid and --api-key from flags passed to handler", async () => {
562
+ setArgv("start", "--veid=12345", "--api-key=secret", "--other=value");
563
+ mockPowerStart.mockResolvedValueOnce({ error: 0 });
285
564
 
286
565
  await main();
287
566
 
288
- expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
289
- JSON.stringify({ error: 0, message: "Started" }),
567
+ expect(mockPowerStart).toHaveBeenCalledExactlyOnceWith(
568
+ [],
569
+ { other: "value" },
570
+ expect.anything(),
290
571
  );
291
- consoleSpy.mockRestore();
292
572
  });
293
573
 
294
- it("writes pretty-printed JSON to stdout", async () => {
295
- setArgv("power", "start");
574
+ // ---- Standalone boolean flag ----
575
+
576
+ it("treats standalone --on flag as flags.on = '1'", async () => {
577
+ setArgv("snapshot", "sticky", "snap1", "--on");
578
+ mockSnapshotSticky.mockResolvedValueOnce({ error: 0 });
579
+
580
+ await main();
581
+
582
+ expect(mockSnapshotSticky).toHaveBeenCalledExactlyOnceWith(
583
+ ["snap1"],
584
+ { on: "1" },
585
+ expect.anything(),
586
+ );
587
+ });
588
+
589
+ // ---- JSON output ----
590
+
591
+ it("writes JSON result to stdout", async () => {
592
+ setArgv("start");
296
593
  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);
594
+ mockPowerStart.mockResolvedValueOnce({ error: 0, message: "Started" });
302
595
 
303
596
  await main();
304
597
 
305
- const logged = consoleSpy.mock.calls[0]?.[0] as string;
306
- expect(() => JSON.parse(logged)).not.toThrow();
307
- expect(JSON.parse(logged)).toEqual(result);
598
+ expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
599
+ JSON.stringify({ error: 0, message: "Started" }),
600
+ );
308
601
  consoleSpy.mockRestore();
309
602
  });
310
603
 
311
604
  // ---- Error handling: stderr + exit code 1 ----
312
605
 
313
606
  it("writes errors to stderr and exits with code 1", async () => {
314
- setArgv("power", "start");
607
+ setArgv("start");
315
608
  const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
316
- mockPowerRun.mockRejectedValueOnce(new Error("API failure"));
609
+ mockPowerStart.mockRejectedValueOnce(new Error("API failure"));
317
610
 
318
611
  await main();
319
612
 
@@ -324,19 +617,8 @@ describe("CLI dispatcher", () => {
324
617
  stderrSpy.mockRestore();
325
618
  });
326
619
 
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
620
  it("exits with code 1 when neither flags nor env vars provide credentials", async () => {
339
- setArgv("power", "start");
621
+ setArgv("start");
340
622
  vi.stubEnv("KIWIVM_VEID", undefined);
341
623
  vi.stubEnv("KIWIVM_API_KEY", undefined);
342
624
  const stderrSpy = vi.spyOn(console, "error").mockImplementation(() => {});
@@ -350,25 +632,30 @@ describe("CLI dispatcher", () => {
350
632
 
351
633
  // ---- Help command ----
352
634
 
353
- it("prints help text when category is 'help'", async () => {
635
+ it("prints help text when command is 'help'", async () => {
354
636
  setArgv("help");
355
637
  const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
638
+ mockHelpRun.mockReturnValue("Usage: kiwivm-cli ...");
356
639
 
357
640
  await main();
358
641
 
359
642
  expect(consoleLogSpy).toHaveBeenCalledWith(
360
643
  expect.stringContaining("kiwivm-cli"),
361
644
  );
645
+ expect(process.exit).not.toHaveBeenCalledWith(1);
362
646
  consoleLogSpy.mockRestore();
363
647
  });
364
648
 
365
- it("prints help and exits successfully for 'help' command", async () => {
366
- setArgv("help");
649
+ it("prints help text when no command provided (empty args)", async () => {
650
+ setArgv();
367
651
  const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
652
+ mockHelpRun.mockReturnValue("Usage: kiwivm-cli ...");
368
653
 
369
654
  await main();
370
655
 
371
- // Help should not call process.exit with an error code
656
+ expect(consoleLogSpy).toHaveBeenCalledWith(
657
+ expect.stringContaining("kiwivm-cli"),
658
+ );
372
659
  expect(process.exit).not.toHaveBeenCalledWith(1);
373
660
  consoleLogSpy.mockRestore();
374
661
  });