kastell 2.2.4 → 2.2.5

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 (75) hide show
  1. package/.claude-plugin/marketplace.json +18 -18
  2. package/.claude-plugin/plugin.json +45 -38
  3. package/CHANGELOG.md +1294 -1266
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +1 -1
  7. package/README.tr.md +1 -1
  8. package/bin/kastell +2 -2
  9. package/bin/kastell-mcp +5 -5
  10. package/dist/adapters/coolify.js +92 -92
  11. package/dist/adapters/dokploy.js +99 -99
  12. package/dist/core/audit/formatters/badge.js +20 -20
  13. package/dist/core/completions.js +631 -631
  14. package/dist/mcp/server.d.ts.map +1 -1
  15. package/dist/mcp/server.js +25 -31
  16. package/dist/mcp/server.js.map +1 -1
  17. package/dist/mcp/tools/serverExplain.d.ts.map +1 -1
  18. package/dist/mcp/tools/serverExplain.js.map +1 -1
  19. package/dist/mcp/tools/serverFleet.d.ts.map +1 -1
  20. package/dist/mcp/tools/serverFleet.js.map +1 -1
  21. package/dist/mcp/tools/serverInfo.d.ts +1 -1
  22. package/dist/mcp/tools/serverInfo.js +1 -1
  23. package/dist/mcp/tools/serverPlugin.d.ts.map +1 -1
  24. package/dist/mcp/tools/serverPlugin.js.map +1 -1
  25. package/dist/mcp-bundle.mjs +101015 -0
  26. package/dist/utils/cloudInit.js +58 -58
  27. package/dist/utils/version.d.ts.map +1 -1
  28. package/dist/utils/version.js +19 -4
  29. package/dist/utils/version.js.map +1 -1
  30. package/kastell-plugin/.claude-plugin/plugin.json +20 -20
  31. package/kastell-plugin/.mcp.json +15 -8
  32. package/kastell-plugin/README.md +113 -113
  33. package/kastell-plugin/agents/kastell-auditor.md +77 -77
  34. package/kastell-plugin/agents/scripts/bucket_mapper.sh +101 -101
  35. package/kastell-plugin/agents/scripts/trend_report.sh +91 -91
  36. package/kastell-plugin/hooks/destroy-block.cjs +31 -31
  37. package/kastell-plugin/hooks/hooks.json +57 -57
  38. package/kastell-plugin/hooks/pre-commit-audit-guard.cjs +75 -75
  39. package/kastell-plugin/hooks/session-audit.cjs +86 -86
  40. package/kastell-plugin/hooks/session-log.cjs +56 -56
  41. package/kastell-plugin/hooks/stop-quality-check.cjs +72 -72
  42. package/kastell-plugin/skills/kastell-careful/SKILL.md +64 -64
  43. package/kastell-plugin/skills/kastell-ops/SKILL.md +139 -139
  44. package/kastell-plugin/skills/kastell-ops/references/commands.md +45 -45
  45. package/kastell-plugin/skills/kastell-ops/references/mcp-tools.md +50 -50
  46. package/kastell-plugin/skills/kastell-ops/references/patterns.md +145 -145
  47. package/kastell-plugin/skills/kastell-ops/references/pitfalls.md +136 -136
  48. package/kastell-plugin/skills/kastell-ops/scripts/check_coverage.sh +101 -101
  49. package/kastell-plugin/skills/kastell-ops/scripts/fleet_report.sh +73 -73
  50. package/kastell-plugin/skills/kastell-ops/scripts/parse_audit.sh +76 -76
  51. package/kastell-plugin/skills/kastell-research/SKILL.md +90 -90
  52. package/kastell-plugin/skills/kastell-scaffold/SKILL.md +104 -104
  53. package/kastell-plugin/skills/kastell-scaffold/references/template-audit-check.md +150 -150
  54. package/kastell-plugin/skills/kastell-scaffold/references/template-command.md +80 -80
  55. package/kastell-plugin/skills/kastell-scaffold/references/template-mcp-tool.md +72 -72
  56. package/kastell-plugin/skills/kastell-scaffold/references/template-provider.md +67 -67
  57. package/kastell-plugin/skills/kastell-scaffold/scripts/scaffold.sh +180 -180
  58. package/kastell-plugin/skills/kastell-scaffold/templates/check-test.ts.tpl +27 -27
  59. package/kastell-plugin/skills/kastell-scaffold/templates/check.ts.tpl +50 -50
  60. package/kastell-plugin/skills/kastell-scaffold/templates/command-core.ts.tpl +18 -18
  61. package/kastell-plugin/skills/kastell-scaffold/templates/command-test.ts.tpl +17 -17
  62. package/kastell-plugin/skills/kastell-scaffold/templates/command.ts.tpl +25 -25
  63. package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool-test.ts.tpl +30 -30
  64. package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool.ts.tpl +29 -29
  65. package/kastell-plugin/skills/kastell-scaffold/templates/provider-test.ts.tpl +34 -34
  66. package/kastell-plugin/skills/kastell-scaffold/templates/provider.ts.tpl +32 -32
  67. package/package.json +125 -122
  68. package/dist/commands/interactive.d.ts +0 -11
  69. package/dist/commands/interactive.d.ts.map +0 -1
  70. package/dist/commands/interactive.js +0 -1079
  71. package/dist/commands/interactive.js.map +0 -1
  72. package/dist/core/lock.d.ts +0 -66
  73. package/dist/core/lock.d.ts.map +0 -1
  74. package/dist/core/lock.js +0 -556
  75. package/dist/core/lock.js.map +0 -1
@@ -1,1079 +0,0 @@
1
- import inquirer from "inquirer";
2
- import chalk from "chalk";
3
- import { listAllProfileNames } from "../core/audit/profiles.js";
4
- import { isValidPort } from "../core/firewall.js";
5
- const BACK = "__back__";
6
- // ─── Shared validators ──────────────────────────────────────────────────────
7
- const validateRequired = (msg) => (v) => v.trim().length > 0 ? true : msg;
8
- const validateScore = (v) => {
9
- const num = Number(v);
10
- return num >= 0 && num <= 100 ? true : "Enter 0-100";
11
- };
12
- const validateColonPair = (msg) => (v) => {
13
- const parts = v.split(":");
14
- return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0
15
- ? true
16
- : msg;
17
- };
18
- const SCHEDULE_COMMANDS = {
19
- "schedule-fix": "fix",
20
- "schedule-audit": "audit",
21
- "schedule-list": "list",
22
- "schedule-remove": "remove",
23
- };
24
- const MENU = [
25
- {
26
- label: "Server Management",
27
- emoji: "\uD83D\uDDA5\uFE0F",
28
- actions: [
29
- { name: "Deploy a new server", value: "init", description: "Provision a VPS on Hetzner, DigitalOcean, Vultr, or Linode" },
30
- { name: "Add an existing server", value: "add", description: "Register an existing server in your Kastell config" },
31
- { name: "List all servers", value: "list", description: "Show all managed servers with status overview" },
32
- { name: "Check server status", value: "status", description: "Check uptime, resources, and platform health" },
33
- { name: "Fleet overview", value: "fleet", description: "Health and security posture of all servers at once" },
34
- { name: "SSH into a server", value: "ssh", description: "Open an SSH session or run a remote command" },
35
- { name: "Restart a server", value: "restart", description: "Reboot a managed server via provider API" },
36
- { name: "Remove from config", value: "remove", description: "Remove a server from local config without destroying it" },
37
- { name: "Destroy a server", value: "destroy", description: "Permanently delete a server from the cloud provider" },
38
- ],
39
- },
40
- {
41
- label: "Security",
42
- emoji: "\uD83D\uDD12",
43
- actions: [
44
- { name: "Run security audit", value: "audit", description: "Score server security across 31 categories with compliance mapping" },
45
- { name: "Harden SSH & fail2ban", value: "secure", description: "Configure SSH security and brute-force protection" },
46
- { name: "Lock server (production hardening)", value: "lock", description: "Apply 24-step hardening: SSH, fail2ban, UFW, sysctl, auditd, AIDE, and more" },
47
- { name: "Fix server (safe auto-fix)", value: "fix", description: "Apply safe fixes automatically with backup (SAFE tier only)" },
48
- { name: "Manage firewall (UFW)", value: "firewall", description: "View, add, or remove UFW firewall port rules" },
49
- { name: "Manage domain & SSL", value: "domain", description: "Set custom domains and configure SSL certificates" },
50
- { name: "Collect forensic evidence", value: "evidence", description: "Gather logs, ports, firewall rules with SHA256 checksums" },
51
- { name: "Manage auth tokens", value: "auth", description: "Store, remove, or list provider API tokens in OS keychain" },
52
- { name: "Regression baseline status", value: "regression-status", description: "Show baseline status for all or specific server" },
53
- { name: "Reset regression baseline", value: "regression-reset", description: "Delete baseline for a server" },
54
- ],
55
- },
56
- {
57
- label: "Monitoring & Logs",
58
- emoji: "\uD83D\uDCCA",
59
- actions: [
60
- { name: "View server logs", value: "logs", description: "View Coolify, Dokploy, Docker, or system logs" },
61
- { name: "Monitor resources (CPU/RAM/Disk)", value: "monitor", description: "Live resource usage with optional Docker container list" },
62
- { name: "Health check", value: "health", description: "Verify platform and server connectivity" },
63
- { name: "Guard daemon", value: "guard", description: "Start, stop, or check autonomous security monitoring" },
64
- { name: "Doctor (diagnostics + auto-fix)", value: "doctor", description: "Proactive health analysis with optional auto-fix" },
65
- ],
66
- },
67
- {
68
- label: "Backup & Snapshots",
69
- emoji: "\uD83D\uDCBE",
70
- actions: [
71
- { name: "Create a backup", value: "backup", description: "Download server configuration backup via SCP" },
72
- { name: "List local backups", value: "backup-list", description: "Show all locally stored backups" },
73
- { name: "Restore from backup", value: "restore", description: "Restore a previously downloaded backup to a server" },
74
- { name: "Manage snapshots", value: "snapshot", description: "List, create, or delete provider-level snapshots" },
75
- ],
76
- },
77
- {
78
- label: "Maintenance",
79
- emoji: "\uD83D\uDD27",
80
- actions: [
81
- { name: "Update platform (Coolify/Dokploy)", value: "update", description: "Update Coolify or Dokploy to the latest version" },
82
- { name: "Full maintenance cycle", value: "maintain", description: "Update + security patches + disk cleanup + Docker prune" },
83
- ],
84
- },
85
- {
86
- label: "Notifications & Bot",
87
- emoji: "\uD83D\uDD14",
88
- actions: [
89
- { name: "Manage notifications", value: "notify", description: "Add Telegram or Discord/Slack webhook for alerts" },
90
- { name: "Start Telegram bot", value: "bot", description: "Start Telegram bot for read-only server commands (foreground)" },
91
- ],
92
- },
93
- {
94
- label: "Scheduling",
95
- emoji: "\u23F0",
96
- actions: [
97
- { name: "Schedule automatic fix runs", value: "schedule-fix", description: "Install a local cron for periodic kastell fix --safe" },
98
- { name: "Schedule automatic audit runs", value: "schedule-audit", description: "Install a local cron for periodic kastell audit" },
99
- { name: "List installed schedules", value: "schedule-list", description: "Show all fix/audit schedules" },
100
- { name: "Remove a schedule", value: "schedule-remove", description: "Remove an installed fix or audit schedule" },
101
- ],
102
- },
103
- {
104
- label: "Configuration",
105
- emoji: "\u2699\uFE0F",
106
- actions: [
107
- { name: "Manage defaults", value: "config", description: "Set default provider, region, and server template" },
108
- { name: "Export server list", value: "export", description: "Export server configuration to a JSON file" },
109
- { name: "Import server list", value: "import", description: "Import servers from a previously exported JSON file" },
110
- { name: "Manage plugins", value: "plugin", description: "Install, remove, list, or validate kastell plugins" },
111
- { name: "Shell completions", value: "completions", description: "Generate bash, zsh, or fish completion scripts" },
112
- { name: "Check version", value: "version", description: "Show current Kastell version and check for updates" },
113
- { name: "View changelog", value: "changelog", description: "Show release notes for the latest or a specific version" },
114
- ],
115
- },
116
- ];
117
- function buildMainChoices() {
118
- const choices = [];
119
- for (const category of MENU) {
120
- choices.push(new inquirer.Separator(chalk.yellow.bold(` ${category.emoji} ${category.label}`)));
121
- for (const action of category.actions) {
122
- choices.push({ name: ` ${action.name}`, value: action.value, description: action.description });
123
- }
124
- }
125
- choices.push(new inquirer.Separator(" "));
126
- choices.push({ name: chalk.dim(" Exit"), value: "exit" });
127
- return choices;
128
- }
129
- export function buildSearchSource(term) {
130
- const all = buildMainChoices();
131
- if (!term)
132
- return all;
133
- const lower = term.toLowerCase();
134
- const filtered = all.filter((c) => {
135
- // Skip separators in filtered results
136
- if ("type" in c && c.type === "separator")
137
- return false;
138
- const choice = c;
139
- return (choice.name.toLowerCase().includes(lower) ||
140
- choice.value.toLowerCase().includes(lower) ||
141
- (choice.description?.toLowerCase().includes(lower) ?? false));
142
- });
143
- // Always include Exit
144
- if (!filtered.some((c) => "value" in c && c.value === "exit")) {
145
- filtered.push({ name: chalk.dim(" Exit"), value: "exit" });
146
- }
147
- return filtered;
148
- }
149
- function backChoice() {
150
- return { name: chalk.dim("\u2190 Back"), value: BACK };
151
- }
152
- // ─── Sub-option prompts ─────────────────────────────────────────────────────
153
- async function promptList(message, choices) {
154
- const { answer } = await inquirer.prompt([
155
- {
156
- type: "list",
157
- name: "answer",
158
- message,
159
- choices: [...choices, new inquirer.Separator(" "), backChoice()],
160
- loop: false,
161
- },
162
- ]);
163
- return answer === BACK ? null : answer;
164
- }
165
- async function promptInit() {
166
- const mode = await promptList("Server mode:", [
167
- { name: "Coolify (auto-install panel)", value: "coolify" },
168
- { name: "Dokploy (auto-install panel)", value: "dokploy" },
169
- { name: "Bare (generic VPS, no panel)", value: "bare" },
170
- ]);
171
- if (!mode)
172
- return null;
173
- const template = await promptList("Server template:", [
174
- { name: "Starter (cheapest option)", value: "starter" },
175
- { name: "Production (more resources)", value: "production" },
176
- { name: "Dev (development)", value: "dev" },
177
- ]);
178
- if (!template)
179
- return null;
180
- const { fullSetup } = await inquirer.prompt([
181
- {
182
- type: "confirm",
183
- name: "fullSetup",
184
- message: "Run full setup after deploy? (firewall + SSH hardening)",
185
- default: true,
186
- },
187
- ]);
188
- const args = ["init", "--mode", mode, "--template", template];
189
- if (fullSetup)
190
- args.push("--full-setup");
191
- return args;
192
- }
193
- async function promptLogs() {
194
- const service = await promptList("Log source:", [
195
- { name: "Coolify container logs", value: "coolify" },
196
- { name: "Dokploy container logs", value: "dokploy" },
197
- { name: "Docker service logs", value: "docker" },
198
- { name: "Full system journal", value: "system" },
199
- ]);
200
- if (!service)
201
- return null;
202
- const lines = await promptList("Number of log lines:", [
203
- { name: "25 lines", value: "25" },
204
- { name: "50 lines (default)", value: "50" },
205
- { name: "100 lines", value: "100" },
206
- { name: "200 lines", value: "200" },
207
- ]);
208
- if (!lines)
209
- return null;
210
- const { follow } = await inquirer.prompt([
211
- { type: "confirm", name: "follow", message: "Follow log output in real-time?", default: false },
212
- ]);
213
- const args = ["logs", "--service", service, "--lines", lines];
214
- if (follow)
215
- args.push("--follow");
216
- return args;
217
- }
218
- async function promptFirewall() {
219
- const sub = await promptList("Firewall action:", [
220
- { name: "Show current rules", value: "status" },
221
- { name: "Initial firewall setup", value: "setup" },
222
- { name: "Add a port rule", value: "add" },
223
- { name: "Remove a port rule", value: "remove" },
224
- ]);
225
- if (!sub)
226
- return null;
227
- if (sub === "add" || sub === "remove") {
228
- const answers = await inquirer.prompt([
229
- {
230
- type: "input",
231
- name: "port",
232
- message: "Port number:",
233
- validate: (v) => isValidPort(Number(v)) || "Enter a valid port (1-65535)",
234
- },
235
- ]);
236
- const protocol = await promptList("Protocol:", [
237
- { name: "TCP", value: "tcp" },
238
- { name: "UDP", value: "udp" },
239
- ]);
240
- if (!protocol)
241
- return null;
242
- return ["firewall", sub, "--port", answers.port, "--protocol", protocol];
243
- }
244
- return ["firewall", sub];
245
- }
246
- async function promptSecure() {
247
- const sub = await promptList("Security action:", [
248
- { name: "Harden SSH + install fail2ban", value: "setup" },
249
- { name: "Run security audit", value: "audit" },
250
- { name: "Show security status", value: "status" },
251
- ]);
252
- if (!sub)
253
- return null;
254
- return ["secure", sub];
255
- }
256
- async function promptDomain() {
257
- const sub = await promptList("Domain action:", [
258
- { name: "Show current domain info", value: "info" },
259
- { name: "List domains", value: "list" },
260
- { name: "Set a custom domain", value: "add" },
261
- { name: "Check DNS for a domain", value: "check" },
262
- { name: "Remove domain", value: "remove" },
263
- ]);
264
- if (!sub)
265
- return null;
266
- if (sub === "add" || sub === "check") {
267
- const { domain } = await inquirer.prompt([
268
- {
269
- type: "input",
270
- name: "domain",
271
- message: "Domain name (e.g. panel.example.com):",
272
- validate: (v) => (v.includes(".") ? true : "Enter a valid domain"),
273
- },
274
- ]);
275
- const args = ["domain", sub, "--domain", domain];
276
- if (sub === "add") {
277
- const { ssl } = await inquirer.prompt([
278
- { type: "confirm", name: "ssl", message: "Enable SSL (HTTPS)?", default: true },
279
- ]);
280
- if (!ssl)
281
- args.push("--no-ssl");
282
- }
283
- return args;
284
- }
285
- return ["domain", sub];
286
- }
287
- async function promptSnapshot() {
288
- const sub = await promptList("Snapshot action:", [
289
- { name: "List snapshots", value: "list" },
290
- { name: "List all servers' snapshots", value: "list-all" },
291
- { name: "Create a snapshot", value: "create" },
292
- { name: "Restore from snapshot", value: "restore" },
293
- { name: "Delete a snapshot", value: "delete" },
294
- ]);
295
- if (!sub)
296
- return null;
297
- if (sub === "list-all")
298
- return ["snapshot", "list", "--all"];
299
- return ["snapshot", sub];
300
- }
301
- async function promptMonitor() {
302
- const mode = await promptList("Monitor options:", [
303
- { name: "Basic (CPU/RAM/Disk)", value: "basic" },
304
- { name: "With Docker containers", value: "containers" },
305
- ]);
306
- if (!mode)
307
- return null;
308
- const args = ["monitor"];
309
- if (mode === "containers")
310
- args.push("--containers");
311
- return args;
312
- }
313
- async function promptMaintain() {
314
- const mode = await promptList("Maintenance mode:", [
315
- { name: "Full cycle (update + reboot)", value: "full" },
316
- { name: "Skip reboot (business hours)", value: "skip-reboot" },
317
- { name: "All servers at once", value: "all" },
318
- { name: "Dry run (preview steps)", value: "dry-run" },
319
- ]);
320
- if (!mode)
321
- return null;
322
- const args = ["maintain"];
323
- if (mode === "skip-reboot")
324
- args.push("--skip-reboot");
325
- else if (mode === "all")
326
- args.push("--all");
327
- else if (mode === "dry-run")
328
- args.push("--dry-run");
329
- return args;
330
- }
331
- async function promptStatus() {
332
- const mode = await promptList("Status check:", [
333
- { name: "Single server", value: "single" },
334
- { name: "All servers at once", value: "all" },
335
- { name: "With auto-restart if platform is down", value: "autostart" },
336
- ]);
337
- if (!mode)
338
- return null;
339
- const args = ["status"];
340
- if (mode === "all")
341
- args.push("--all");
342
- if (mode === "autostart")
343
- args.push("--autostart");
344
- return args;
345
- }
346
- async function promptUpdate() {
347
- const mode = await promptList("Update scope:", [
348
- { name: "Single server", value: "single" },
349
- { name: "All servers at once", value: "all" },
350
- ]);
351
- if (!mode)
352
- return null;
353
- const args = ["update"];
354
- if (mode === "all")
355
- args.push("--all");
356
- return args;
357
- }
358
- async function promptDoctor() {
359
- const mode = await promptList("Doctor mode:", [
360
- { name: "Fresh data via SSH (accurate)", value: "fresh" },
361
- { name: "Use cached metrics (fast)", value: "cached" },
362
- { name: "Interactive fix mode", value: "fix" },
363
- { name: "Auto-fix (diagnose + fix all)", value: "auto-fix" },
364
- { name: "Auto-fix dry run (preview only)", value: "auto-fix-dry" },
365
- { name: "JSON output", value: "json" },
366
- { name: "Check local tokens (no server)", value: "check-tokens" },
367
- ]);
368
- if (!mode)
369
- return null;
370
- if (mode === "check-tokens")
371
- return ["doctor", "--check-tokens"];
372
- if (mode === "json")
373
- return ["doctor", "--fresh", "--json"];
374
- const args = ["doctor"];
375
- if (mode === "fresh")
376
- args.push("--fresh");
377
- if (mode === "fix") {
378
- args.push("--fix");
379
- const dryRun = await promptList("Fix mode:", [
380
- { name: "Execute fixes interactively", value: "live" },
381
- { name: "Dry run (show commands only)", value: "dry-run" },
382
- ]);
383
- if (!dryRun)
384
- return null;
385
- if (dryRun === "dry-run")
386
- args.push("--dry-run");
387
- }
388
- if (mode === "auto-fix") {
389
- args.push("--auto-fix");
390
- const forceOption = await promptList("Confirmation mode:", [
391
- { name: "Confirm each finding", value: "interactive" },
392
- { name: "Skip confirmations (--force)", value: "force" },
393
- ]);
394
- if (!forceOption)
395
- return null;
396
- if (forceOption === "force")
397
- args.push("--force");
398
- }
399
- if (mode === "auto-fix-dry") {
400
- args.push("--auto-fix", "--dry-run");
401
- }
402
- return args;
403
- }
404
- async function promptAuth() {
405
- const sub = await promptList("Auth action:", [
406
- { name: "List stored tokens", value: "list" },
407
- { name: "Store a provider token", value: "set" },
408
- { name: "Remove a provider token", value: "remove" },
409
- ]);
410
- if (!sub)
411
- return null;
412
- if (sub === "set" || sub === "remove") {
413
- const provider = await promptList("Provider:", [
414
- { name: "Hetzner Cloud", value: "hetzner" },
415
- { name: "DigitalOcean", value: "digitalocean" },
416
- { name: "Vultr", value: "vultr" },
417
- { name: "Linode", value: "linode" },
418
- ]);
419
- if (!provider)
420
- return null;
421
- return ["auth", sub, provider];
422
- }
423
- return ["auth", sub];
424
- }
425
- async function promptSsh() {
426
- const mode = await promptList("SSH mode:", [
427
- { name: "Open interactive SSH session", value: "interactive" },
428
- { name: "Run a single command", value: "command" },
429
- ]);
430
- if (!mode)
431
- return null;
432
- if (mode === "command") {
433
- const { command } = await inquirer.prompt([
434
- { type: "input", name: "command", message: "Command to execute:" },
435
- ]);
436
- return ["ssh", "--command", command];
437
- }
438
- return ["ssh"];
439
- }
440
- async function promptBackup() {
441
- const sub = await promptList("Backup action:", [
442
- { name: "Create a new backup", value: "create" },
443
- { name: "Backup all servers", value: "all" },
444
- { name: "Dry run (preview)", value: "dry-run" },
445
- { name: "Manage backup schedule", value: "schedule" },
446
- ]);
447
- if (!sub)
448
- return null;
449
- if (sub === "schedule") {
450
- const schedAction = await promptList("Backup schedule:", [
451
- { name: "Set cron schedule", value: "set" },
452
- { name: "List current schedule", value: "list" },
453
- { name: "Remove schedule", value: "remove" },
454
- ]);
455
- if (!schedAction)
456
- return null;
457
- if (schedAction === "list")
458
- return ["backup", "--schedule", "list"];
459
- if (schedAction === "remove")
460
- return ["backup", "--schedule", "remove"];
461
- const { cron } = await inquirer.prompt([{
462
- type: "input",
463
- name: "cron",
464
- message: "Cron expression (e.g. 0 2 * * *):",
465
- validate: validateRequired("Cron expression required"),
466
- }]);
467
- return ["backup", "--schedule", cron];
468
- }
469
- const args = ["backup"];
470
- if (sub === "all")
471
- args.push("--all");
472
- if (sub === "dry-run")
473
- args.push("--dry-run");
474
- return args;
475
- }
476
- async function promptImport() {
477
- const action = await promptList("Import server list:", [
478
- { name: "Import from JSON file", value: "file" },
479
- ]);
480
- if (!action)
481
- return null;
482
- const { path } = await inquirer.prompt([
483
- {
484
- type: "input",
485
- name: "path",
486
- message: "Path to JSON file to import:",
487
- validate: validateRequired("File path is required"),
488
- },
489
- ]);
490
- return ["import", path];
491
- }
492
- async function promptAudit() {
493
- const mode = await promptList("Audit mode:", [
494
- { name: "Run full audit", value: "run" },
495
- { name: "Run with --explain (show fixes)", value: "explain" },
496
- { name: "Compare two snapshots (diff)", value: "diff" },
497
- { name: "Interactive fix mode", value: "fix" },
498
- { name: "List all checks (no scan)", value: "list-checks" },
499
- { name: "Explain a specific check (deep-dive)", value: "explain-check" },
500
- { name: "Run with compliance profile", value: "profile" },
501
- { name: "Compliance framework report", value: "compliance" },
502
- { name: "Save snapshot", value: "snapshot" },
503
- { name: "List saved snapshots", value: "snapshots" },
504
- { name: "Compare two servers", value: "compare" },
505
- { name: "Score trend over time", value: "trend" },
506
- { name: "Watch mode (auto-refresh)", value: "watch" },
507
- { name: "Audit unregistered server by IP", value: "host" },
508
- { name: "CI gate (exit 1 if below threshold)", value: "threshold" },
509
- { name: "Generate report (HTML/Markdown)", value: "report" },
510
- ]);
511
- if (!mode)
512
- return null;
513
- if (mode === "explain")
514
- return ["audit", "--explain"];
515
- if (mode === "diff") {
516
- const { diffRef } = await inquirer.prompt([
517
- {
518
- type: "input",
519
- name: "diffRef",
520
- message: "Diff reference (e.g. pre-upgrade:latest or pre-upgrade:post-upgrade):",
521
- validate: validateColonPair("Format: before:after (e.g. pre-upgrade:latest)"),
522
- },
523
- ]);
524
- return ["audit", "--diff", diffRef];
525
- }
526
- if (mode === "fix") {
527
- const dryRun = await promptList("Fix mode:", [
528
- { name: "Execute fixes interactively", value: "live" },
529
- { name: "Dry run (show commands only)", value: "dry-run" },
530
- ]);
531
- if (!dryRun)
532
- return null;
533
- const args = ["audit", "--fix"];
534
- if (dryRun === "dry-run")
535
- args.push("--dry-run");
536
- return args;
537
- }
538
- if (mode === "list-checks")
539
- return ["audit", "--list-checks"];
540
- if (mode === "explain-check") {
541
- const { checkId } = await inquirer.prompt([
542
- {
543
- type: "input",
544
- name: "checkId",
545
- message: "Enter check ID (e.g. SSH-PASSWORD-AUTH):",
546
- },
547
- ]);
548
- if (!checkId?.trim())
549
- return null;
550
- return ["explain", checkId.trim()];
551
- }
552
- if (mode === "snapshot") {
553
- const { snapName } = await inquirer.prompt([
554
- { type: "input", name: "snapName", message: "Snapshot name (leave empty for auto):", default: "" },
555
- ]);
556
- return snapName ? ["audit", "--snapshot", snapName] : ["audit", "--snapshot"];
557
- }
558
- if (mode === "snapshots")
559
- return ["audit", "--snapshots"];
560
- if (mode === "compare") {
561
- const { compareRef } = await inquirer.prompt([
562
- {
563
- type: "input",
564
- name: "compareRef",
565
- message: "Compare (server1:server2):",
566
- validate: validateColonPair("Format: server1:server2"),
567
- },
568
- ]);
569
- const compareMode = await promptList("Compare mode:", [
570
- { name: "Category summary (default)", value: "summary" },
571
- { name: "Check-level diff (detailed)", value: "detail" },
572
- ]);
573
- if (!compareMode)
574
- return null;
575
- const args = ["audit", "--compare", compareRef];
576
- if (compareMode === "detail")
577
- args.push("--detail");
578
- return args;
579
- }
580
- if (mode === "trend") {
581
- const days = await promptList("Time range:", [
582
- { name: "Last 7 days", value: "7" },
583
- { name: "Last 30 days", value: "30" },
584
- { name: "All time", value: "0" },
585
- ]);
586
- if (!days)
587
- return null;
588
- return days === "0" ? ["audit", "--trend"] : ["audit", "--trend", "--days", days];
589
- }
590
- if (mode === "watch") {
591
- const interval = await promptList("Refresh interval:", [
592
- { name: "30 seconds", value: "30" },
593
- { name: "60 seconds", value: "60" },
594
- { name: "300 seconds (5 min)", value: "300" },
595
- ]);
596
- if (!interval)
597
- return null;
598
- return ["audit", "--watch", interval];
599
- }
600
- if (mode === "host") {
601
- const { hostAddr } = await inquirer.prompt([
602
- {
603
- type: "input",
604
- name: "hostAddr",
605
- message: "Server address (user@ip):",
606
- validate: (v) => (v.includes("@") ? true : "Format: user@ip"),
607
- },
608
- ]);
609
- return ["audit", "--host", hostAddr];
610
- }
611
- if (mode === "threshold") {
612
- const { thresholdScore } = await inquirer.prompt([
613
- {
614
- type: "input",
615
- name: "thresholdScore",
616
- message: "Minimum score (exit 1 if below):",
617
- validate: validateScore,
618
- },
619
- ]);
620
- return ["audit", "--threshold", thresholdScore];
621
- }
622
- if (mode === "report") {
623
- const reportFormat = await promptList("Report format:", [
624
- { name: "Markdown (.md)", value: "md" },
625
- { name: "HTML (.html)", value: "html" },
626
- ]);
627
- if (!reportFormat)
628
- return null;
629
- return ["audit", "--report", reportFormat];
630
- }
631
- if (mode === "profile") {
632
- const profile = await promptList("Compliance profile:", [
633
- { name: "CIS Level 1 (essential)", value: "cis-level1" },
634
- { name: "CIS Level 2 (advanced)", value: "cis-level2" },
635
- { name: "PCI-DSS (payment)", value: "pci-dss" },
636
- { name: "HIPAA (healthcare)", value: "hipaa" },
637
- ]);
638
- if (!profile)
639
- return null;
640
- const format = await promptList("Output format:", [
641
- { name: "Dashboard summary", value: "summary" },
642
- { name: "JSON output", value: "json" },
643
- { name: "Score only", value: "score-only" },
644
- ]);
645
- if (!format)
646
- return null;
647
- const args = ["audit", "--profile", profile];
648
- if (format === "json")
649
- args.push("--json");
650
- else if (format === "score-only")
651
- args.push("--score-only");
652
- else
653
- args.push("--summary");
654
- return args;
655
- }
656
- if (mode === "compliance") {
657
- const { frameworks } = await inquirer.prompt([
658
- {
659
- type: "checkbox",
660
- name: "frameworks",
661
- message: "Select compliance frameworks:",
662
- choices: [
663
- { name: "CIS Benchmark", value: "cis" },
664
- { name: "PCI-DSS", value: "pci-dss" },
665
- { name: "HIPAA", value: "hipaa" },
666
- ],
667
- validate: (v) => (v.length > 0 ? true : "Select at least one framework"),
668
- },
669
- ]);
670
- return ["audit", "--compliance", frameworks.join(",")];
671
- }
672
- // mode === "run" — standard audit
673
- const format = await promptList("Output format:", [
674
- { name: "Dashboard summary", value: "summary" },
675
- { name: "JSON output", value: "json" },
676
- { name: "Score only", value: "score-only" },
677
- { name: "SVG badge", value: "badge" },
678
- { name: "Show score trend", value: "trend" },
679
- ]);
680
- if (!format)
681
- return null;
682
- const args = ["audit"];
683
- if (format === "summary")
684
- args.push("--summary");
685
- else if (format === "json")
686
- args.push("--json");
687
- else if (format === "score-only")
688
- args.push("--score-only");
689
- else if (format === "badge")
690
- args.push("--badge");
691
- else if (format === "trend")
692
- args.push("--trend");
693
- // Optional category/severity filters (AUX-01, AUX-02)
694
- const filter = await promptList("Filter results?", [
695
- { name: "No filter (show all)", value: "none" },
696
- { name: "Filter by category", value: "category" },
697
- { name: "Filter by severity", value: "severity" },
698
- { name: "Filter by both", value: "both" },
699
- ]);
700
- if (!filter)
701
- return null;
702
- if (filter === "category" || filter === "both") {
703
- const TOP_CATEGORIES = [
704
- { name: "SSH", value: "ssh" },
705
- { name: "Firewall", value: "firewall" },
706
- { name: "Updates", value: "updates" },
707
- { name: "Auth", value: "auth" },
708
- { name: "Docker", value: "docker" },
709
- { name: "Network", value: "network" },
710
- { name: "Kernel", value: "kernel" },
711
- { name: "Logging", value: "logging" },
712
- ];
713
- const ALL_CATEGORIES = [
714
- ...TOP_CATEGORIES,
715
- { name: "Filesystem", value: "filesystem" },
716
- { name: "Accounts", value: "accounts" },
717
- { name: "Services", value: "services" },
718
- { name: "Boot", value: "boot" },
719
- { name: "Scheduling", value: "scheduling" },
720
- { name: "Time", value: "time" },
721
- { name: "Banners", value: "banners" },
722
- { name: "Crypto", value: "crypto" },
723
- { name: "File Integrity", value: "file integrity" },
724
- { name: "Malware", value: "malware" },
725
- { name: "MAC", value: "mac" },
726
- { name: "Memory", value: "memory" },
727
- { name: "Secrets", value: "secrets" },
728
- { name: "Cloud Metadata", value: "cloud metadata" },
729
- { name: "Supply Chain", value: "supply chain" },
730
- { name: "Backup Hygiene", value: "backup hygiene" },
731
- { name: "Resource Limits", value: "resource limits" },
732
- { name: "Incident Readiness", value: "incident readiness" },
733
- { name: "DNS Security", value: "dns security" },
734
- ];
735
- const category = await promptList("Category:", [
736
- ...TOP_CATEGORIES,
737
- { name: "Show all 31 categories...", value: "__all__" },
738
- ]);
739
- if (!category)
740
- return null;
741
- if (category === "__all__") {
742
- const fullCategory = await promptList("Category:", ALL_CATEGORIES);
743
- if (!fullCategory)
744
- return null;
745
- args.push("--category", fullCategory);
746
- }
747
- else {
748
- args.push("--category", category);
749
- }
750
- }
751
- if (filter === "severity" || filter === "both") {
752
- const severity = await promptList("Severity:", [
753
- { name: "Critical only", value: "critical" },
754
- { name: "Warning", value: "warning" },
755
- { name: "Info", value: "info" },
756
- ]);
757
- if (!severity)
758
- return null;
759
- args.push("--severity", severity);
760
- }
761
- return args;
762
- }
763
- async function promptLock() {
764
- const mode = await promptList("Lock mode:", [
765
- { name: "Dry run (preview changes)", value: "dry-run" },
766
- { name: "Apply production hardening", value: "production" },
767
- { name: "Apply production (skip confirmation)", value: "production-force" },
768
- ]);
769
- if (!mode)
770
- return null;
771
- const args = ["lock"];
772
- if (mode === "dry-run")
773
- args.push("--dry-run");
774
- else if (mode === "production-force")
775
- args.push("--production", "--force");
776
- else
777
- args.push("--production");
778
- return args;
779
- }
780
- async function promptFix() {
781
- const group = await promptList("Fix options:", [
782
- { name: "Apply fixes", value: "apply" },
783
- { name: "Fix history", value: "history" },
784
- ]);
785
- if (!group)
786
- return null;
787
- if (group === "apply") {
788
- const mode = await promptList("Apply mode:", [
789
- { name: "Dry run (preview safe fixes)", value: "dry-run" },
790
- { name: "Apply safe fixes (backup + fix + verify)", value: "apply" },
791
- { name: "Apply with profile filter", value: "profile" },
792
- { name: "Apply with category filter", value: "category" },
793
- { name: "Apply top N fixes by impact", value: "top" },
794
- { name: "Apply until target score", value: "target" },
795
- { name: "Apply with diff preview", value: "diff" },
796
- { name: "Apply and generate report", value: "report" },
797
- ]);
798
- if (!mode)
799
- return null;
800
- if (mode === "dry-run")
801
- return ["fix", "--safe", "--dry-run"];
802
- if (mode === "apply")
803
- return ["fix", "--safe"];
804
- if (mode === "diff")
805
- return ["fix", "--safe", "--diff"];
806
- if (mode === "report")
807
- return ["fix", "--safe", "--report"];
808
- if (mode === "profile") {
809
- const profileNames = listAllProfileNames();
810
- const choices = profileNames.map((p) => ({ name: p, value: p }));
811
- const profile = await promptList("Fix profile:", choices);
812
- if (!profile)
813
- return null;
814
- return ["fix", "--safe", "--profile", profile];
815
- }
816
- if (mode === "category") {
817
- const { cats } = await inquirer.prompt([{
818
- type: "input",
819
- name: "cats",
820
- message: "Category filter (comma-separated, e.g. Auth,Kernel):",
821
- validate: validateRequired("Enter at least one category"),
822
- }]);
823
- return ["fix", "--safe", "--category", cats];
824
- }
825
- if (mode === "top") {
826
- const { n } = await inquirer.prompt([{
827
- type: "input",
828
- name: "n",
829
- message: "Number of fixes to apply:",
830
- validate: (v) => {
831
- const num = Number(v);
832
- return num >= 1 && Number.isInteger(num) ? true : "Enter a positive integer";
833
- },
834
- }]);
835
- return ["fix", "--safe", "--top", n];
836
- }
837
- if (mode === "target") {
838
- const { score } = await inquirer.prompt([{
839
- type: "input",
840
- name: "score",
841
- message: "Target score (0-100):",
842
- validate: validateScore,
843
- }]);
844
- return ["fix", "--safe", "--target", score];
845
- }
846
- }
847
- if (group === "history") {
848
- const action = await promptList("History action:", [
849
- { name: "View fix history", value: "view" },
850
- { name: "Rollback a specific fix", value: "rollback" },
851
- { name: "Rollback all fixes", value: "rollback-all" },
852
- { name: "Rollback down to a specific fix", value: "rollback-to" },
853
- ]);
854
- if (!action)
855
- return null;
856
- if (action === "view")
857
- return ["fix", "--history"];
858
- if (action === "rollback-all")
859
- return ["fix", "--rollback-all"];
860
- if (action === "rollback-to") {
861
- const { fixId } = await inquirer.prompt([{
862
- type: "input",
863
- name: "fixId",
864
- message: "Rollback down to fix ID:",
865
- validate: validateRequired("Fix ID required"),
866
- }]);
867
- return ["fix", "--rollback-to", fixId];
868
- }
869
- if (action === "rollback") {
870
- const { fixId } = await inquirer.prompt([{
871
- type: "input",
872
- name: "fixId",
873
- message: "Fix ID (or 'last'):",
874
- validate: validateRequired("Fix ID required"),
875
- }]);
876
- return ["fix", "--rollback", fixId];
877
- }
878
- }
879
- return null;
880
- }
881
- async function promptEvidence() {
882
- const action = await promptList("Evidence collection:", [
883
- { name: "Collect with default label", value: "default" },
884
- { name: "Collect with custom label", value: "custom" },
885
- { name: "Collect (overwrite existing)", value: "force" },
886
- { name: "Collect (JSON manifest output)", value: "json" },
887
- ]);
888
- if (!action)
889
- return null;
890
- const args = ["evidence"];
891
- if (action === "force")
892
- args.push("--force");
893
- if (action === "json")
894
- args.push("--json");
895
- if (action === "custom") {
896
- const { name } = await inquirer.prompt([
897
- {
898
- type: "input",
899
- name: "name",
900
- message: "Evidence label (e.g. pre-incident, weekly-check):",
901
- default: "manual",
902
- },
903
- ]);
904
- args.push("--name", name);
905
- }
906
- else {
907
- args.push("--name", "manual");
908
- }
909
- const options = await promptList("Collection options:", [
910
- { name: "Full collection (default)", value: "full" },
911
- { name: "Skip Docker data", value: "no-docker" },
912
- { name: "Skip system info", value: "no-sysinfo" },
913
- { name: "Skip Docker + system info", value: "no-both" },
914
- ]);
915
- if (!options)
916
- return null;
917
- if (options === "no-docker" || options === "no-both")
918
- args.push("--no-docker");
919
- if (options === "no-sysinfo" || options === "no-both")
920
- args.push("--no-sysinfo");
921
- const lines = await promptList("Log lines to collect:", [
922
- { name: "100 lines", value: "100" },
923
- { name: "500 lines (default)", value: "500" },
924
- { name: "1000 lines", value: "1000" },
925
- ]);
926
- if (!lines)
927
- return null;
928
- if (lines !== "500")
929
- args.push("--lines", lines);
930
- return args;
931
- }
932
- async function promptGuard() {
933
- const sub = await promptList("Guard action:", [
934
- { name: "Check guard status", value: "status" },
935
- { name: "Start guard daemon", value: "start" },
936
- { name: "Stop guard daemon", value: "stop" },
937
- ]);
938
- if (!sub)
939
- return null;
940
- return ["guard", sub];
941
- }
942
- async function promptNotify() {
943
- const sub = await promptList("Notification action:", [
944
- { name: "List notification channels", value: "list" },
945
- { name: "Add a notification channel", value: "add" },
946
- { name: "Remove a notification channel", value: "remove" },
947
- { name: "Send a test notification", value: "test" },
948
- ]);
949
- if (!sub)
950
- return null;
951
- return ["notify", sub];
952
- }
953
- async function promptFleet() {
954
- const mode = await promptList("Fleet output:", [
955
- { name: "Dashboard (default)", value: "default" },
956
- { name: "JSON output", value: "json" },
957
- { name: "Sort by score", value: "sort-score" },
958
- { name: "Sort by provider", value: "sort-provider" },
959
- ]);
960
- if (!mode)
961
- return null;
962
- const args = ["fleet"];
963
- if (mode === "json")
964
- args.push("--json");
965
- if (mode === "sort-score")
966
- args.push("--sort", "score");
967
- if (mode === "sort-provider")
968
- args.push("--sort", "provider");
969
- return args;
970
- }
971
- async function promptCompletions() {
972
- const shell = await promptList("Shell:", [
973
- { name: "Bash", value: "bash" },
974
- { name: "Zsh", value: "zsh" },
975
- { name: "Fish", value: "fish" },
976
- ]);
977
- if (!shell)
978
- return null;
979
- return ["completions", shell];
980
- }
981
- async function promptPlugin() {
982
- const sub = await promptList("Plugin action:", [
983
- { name: "List installed plugins", value: "list" },
984
- { name: "Install a plugin", value: "install" },
985
- { name: "Remove a plugin", value: "remove" },
986
- { name: "Validate plugins", value: "validate" },
987
- ]);
988
- if (!sub)
989
- return null;
990
- if (sub === "install") {
991
- const { name } = await inquirer.prompt([
992
- { type: "input", name: "name", message: "Plugin name (kastell-plugin-<name>):" },
993
- ]);
994
- if (!name)
995
- return null;
996
- return ["plugin", "install", name];
997
- }
998
- if (sub === "remove") {
999
- const { name } = await inquirer.prompt([
1000
- { type: "input", name: "name", message: "Plugin name to remove:" },
1001
- ]);
1002
- if (!name)
1003
- return null;
1004
- return ["plugin", "remove", name];
1005
- }
1006
- return ["plugin", sub];
1007
- }
1008
- // ─── Command → args mapping ─────────────────────────────────────────────────
1009
- const SUB_PROMPTS = {
1010
- init: promptInit,
1011
- auth: promptAuth,
1012
- logs: promptLogs,
1013
- firewall: promptFirewall,
1014
- secure: promptSecure,
1015
- domain: promptDomain,
1016
- snapshot: promptSnapshot,
1017
- monitor: promptMonitor,
1018
- maintain: promptMaintain,
1019
- status: promptStatus,
1020
- update: promptUpdate,
1021
- doctor: promptDoctor,
1022
- ssh: promptSsh,
1023
- backup: promptBackup,
1024
- import: promptImport,
1025
- audit: promptAudit,
1026
- lock: promptLock,
1027
- fix: promptFix,
1028
- evidence: promptEvidence,
1029
- guard: promptGuard,
1030
- fleet: promptFleet,
1031
- notify: promptNotify,
1032
- completions: promptCompletions,
1033
- plugin: promptPlugin,
1034
- };
1035
- const DIRECT_COMMANDS = new Set([
1036
- "list", "add", "destroy", "restart", "remove", "restore", "export", "config",
1037
- "health", "backup-list", "version", "changelog",
1038
- "regression-status", "regression-reset",
1039
- ]);
1040
- export async function interactiveMenu() {
1041
- // Header is printed by index.ts before calling interactiveMenu()
1042
- // Loop: back from sub-menus returns here
1043
- for (;;) {
1044
- const { action } = await inquirer.prompt([
1045
- {
1046
- type: "search",
1047
- name: "action",
1048
- message: "What would you like to do?",
1049
- source: buildSearchSource,
1050
- pageSize: 25,
1051
- },
1052
- ]);
1053
- if (action === "exit")
1054
- return null;
1055
- // Special compound commands
1056
- if (action === "backup-list")
1057
- return ["backup", "list"];
1058
- if (action === "regression-status")
1059
- return ["regression", "status"];
1060
- if (action === "regression-reset")
1061
- return ["regression", "reset"];
1062
- if (action === "bot")
1063
- return ["bot", "start"];
1064
- if (action in SCHEDULE_COMMANDS)
1065
- return ["schedule", SCHEDULE_COMMANDS[action]];
1066
- if (DIRECT_COMMANDS.has(action)) {
1067
- return [action];
1068
- }
1069
- const promptFn = SUB_PROMPTS[action];
1070
- if (promptFn) {
1071
- const result = await promptFn();
1072
- if (result === null)
1073
- continue; // back → show main menu again
1074
- return result;
1075
- }
1076
- return [action];
1077
- }
1078
- }
1079
- //# sourceMappingURL=interactive.js.map