@kill-switch/cli 0.2.0 → 0.3.1

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 (46) hide show
  1. package/README.md +49 -4
  2. package/dist/commands/accounts.d.ts +2 -1
  3. package/dist/commands/accounts.js +44 -27
  4. package/dist/commands/activity.d.ts +3 -0
  5. package/dist/commands/activity.js +80 -0
  6. package/dist/commands/agent-guard.d.ts +10 -0
  7. package/dist/commands/agent-guard.js +175 -0
  8. package/dist/commands/alerts.d.ts +2 -1
  9. package/dist/commands/alerts.js +112 -12
  10. package/dist/commands/analytics.d.ts +2 -1
  11. package/dist/commands/analytics.js +36 -14
  12. package/dist/commands/auth.d.ts +2 -1
  13. package/dist/commands/auth.js +86 -46
  14. package/dist/commands/check.d.ts +2 -1
  15. package/dist/commands/check.js +28 -15
  16. package/dist/commands/config-cmd.js +20 -5
  17. package/dist/commands/kill.d.ts +2 -1
  18. package/dist/commands/kill.js +52 -37
  19. package/dist/commands/onboard.d.ts +2 -17
  20. package/dist/commands/onboard.js +244 -61
  21. package/dist/commands/orgs.d.ts +3 -0
  22. package/dist/commands/orgs.js +192 -0
  23. package/dist/commands/providers.d.ts +3 -0
  24. package/dist/commands/providers.js +82 -0
  25. package/dist/commands/rules.d.ts +2 -1
  26. package/dist/commands/rules.js +51 -28
  27. package/dist/commands/shield.d.ts +2 -1
  28. package/dist/commands/shield.js +36 -17
  29. package/dist/commands/status.d.ts +3 -0
  30. package/dist/commands/status.js +100 -0
  31. package/dist/commands/watch.d.ts +3 -0
  32. package/dist/commands/watch.js +68 -0
  33. package/dist/config.d.ts +4 -1
  34. package/dist/config.js +19 -2
  35. package/dist/device-flow.d.ts +33 -0
  36. package/dist/device-flow.js +91 -0
  37. package/dist/index.js +38 -11
  38. package/dist/output.d.ts +26 -4
  39. package/dist/output.js +101 -12
  40. package/dist/prompts.d.ts +13 -0
  41. package/dist/prompts.js +24 -0
  42. package/dist/types.d.ts +2 -0
  43. package/dist/types.js +1 -0
  44. package/dist/version.d.ts +1 -0
  45. package/dist/version.js +4 -0
  46. package/package.json +29 -7
@@ -1,6 +1,5 @@
1
- import { apiRequest } from "../api-client.js";
2
- import { outputJson, formatTable, outputError } from "../output.js";
3
- export function registerAlertCommands(program) {
1
+ import { outputJson, formatTable, handleError, spinner, success } from "../output.js";
2
+ export function registerAlertCommands(program, createClient) {
4
3
  const alerts = program.command("alerts").description("Manage alert channels");
5
4
  alerts
6
5
  .command("list")
@@ -9,22 +8,120 @@ export function registerAlertCommands(program) {
9
8
  .action(async () => {
10
9
  const json = program.opts().json;
11
10
  try {
12
- const data = await apiRequest("/alerts/channels");
13
- const channels = data.channels || data;
11
+ const client = createClient();
12
+ const channels = await client.alerts.channels();
14
13
  if (json) {
15
14
  outputJson(channels);
16
15
  }
17
16
  else {
18
- formatTable(Array.isArray(channels) ? channels : [], [
17
+ formatTable(channels, [
19
18
  { key: "type", header: "Type" },
20
19
  { key: "name", header: "Name" },
21
20
  { key: "enabled", header: "Enabled" },
21
+ { key: "configPreview", header: "Config" },
22
22
  ]);
23
23
  }
24
24
  }
25
25
  catch (err) {
26
- outputError(err.message, json);
27
- process.exit(1);
26
+ handleError(err, json);
27
+ }
28
+ });
29
+ alerts
30
+ .command("add")
31
+ .description("Add an alert channel")
32
+ .requiredOption("--type <type>", "Channel type: pagerduty | slack | discord | webhook | email | github")
33
+ .option("--name <name>", "Display name for this channel")
34
+ .option("--routing-key <key>", "PagerDuty Events API v2 routing key")
35
+ .option("--webhook-url <url>", "Webhook URL (Slack, Discord, or custom webhook)")
36
+ .option("--email <address>", "Email address")
37
+ // GitHub options
38
+ .option("--token <token>", "GitHub Personal Access Token (repo + workflow scopes)")
39
+ .option("--repo-owner <owner>", "GitHub repository owner / org")
40
+ .option("--repo-name <name>", "GitHub repository name")
41
+ .option("--workflow <file>", "Workflow filename (default: kill-switch-remediate.yml)")
42
+ .option("--branch <ref>", "Branch to dispatch on (default: main)")
43
+ .action(async (opts) => {
44
+ const json = program.opts().json;
45
+ const s = json ? null : spinner("Adding alert channel...").start();
46
+ try {
47
+ const type = opts.type;
48
+ const validTypes = ["pagerduty", "slack", "discord", "webhook", "email", "github"];
49
+ if (!validTypes.includes(type)) {
50
+ throw new Error(`Unknown type "${type}". Valid types: ${validTypes.join(", ")}`);
51
+ }
52
+ // Build config and validate required fields per type
53
+ const config = {};
54
+ if (type === "pagerduty") {
55
+ if (!opts.routingKey)
56
+ throw new Error("--routing-key is required for pagerduty");
57
+ config.routingKey = opts.routingKey;
58
+ }
59
+ else if (type === "slack" || type === "discord" || type === "webhook") {
60
+ if (!opts.webhookUrl)
61
+ throw new Error(`--webhook-url is required for ${type}`);
62
+ config.webhookUrl = opts.webhookUrl;
63
+ }
64
+ else if (type === "email") {
65
+ if (!opts.email)
66
+ throw new Error("--email is required for email");
67
+ config.email = opts.email;
68
+ }
69
+ else if (type === "github") {
70
+ if (!opts.token)
71
+ throw new Error("--token is required for github");
72
+ if (!opts.repoOwner)
73
+ throw new Error("--repo-owner is required for github");
74
+ if (!opts.repoName)
75
+ throw new Error("--repo-name is required for github");
76
+ config.githubToken = opts.token;
77
+ config.repoOwner = opts.repoOwner;
78
+ config.repoName = opts.repoName;
79
+ config.workflowFile = opts.workflow || "kill-switch-remediate.yml";
80
+ config.branchRef = opts.branch || "main";
81
+ }
82
+ const channel = {
83
+ type,
84
+ name: opts.name || type.charAt(0).toUpperCase() + type.slice(1),
85
+ enabled: true,
86
+ config,
87
+ };
88
+ const client = createClient();
89
+ const result = await client.alerts.addChannel(channel);
90
+ s?.stop();
91
+ if (json) {
92
+ outputJson(result);
93
+ }
94
+ else {
95
+ success(`Channel added. You now have ${result.channelCount} alert channel(s).`);
96
+ }
97
+ }
98
+ catch (err) {
99
+ s?.stop();
100
+ handleError(err, json);
101
+ }
102
+ });
103
+ alerts
104
+ .command("remove")
105
+ .alias("rm")
106
+ .description("Remove an alert channel by name")
107
+ .argument("<name>", "Channel name to remove")
108
+ .action(async (name) => {
109
+ const json = program.opts().json;
110
+ const s = json ? null : spinner(`Removing channel "${name}"...`).start();
111
+ try {
112
+ const client = createClient();
113
+ const result = await client.alerts.removeChannel(name);
114
+ s?.stop();
115
+ if (json) {
116
+ outputJson(result);
117
+ }
118
+ else {
119
+ success(`Channel removed. ${result.channelCount} channel(s) remaining.`);
120
+ }
121
+ }
122
+ catch (err) {
123
+ s?.stop();
124
+ handleError(err, json);
28
125
  }
29
126
  });
30
127
  alerts
@@ -32,18 +129,21 @@ export function registerAlertCommands(program) {
32
129
  .description("Send a test alert to all channels")
33
130
  .action(async () => {
34
131
  const json = program.opts().json;
132
+ const s = json ? null : spinner("Sending test alert...").start();
35
133
  try {
36
- const data = await apiRequest("/alerts/test", { method: "POST" });
134
+ const client = createClient();
135
+ const data = await client.alerts.test();
136
+ s?.stop();
37
137
  if (json) {
38
138
  outputJson(data);
39
139
  }
40
140
  else {
41
- console.log("Test alert sent to all configured channels.");
141
+ success(`Test alert sent to ${data.channelsSent} channel(s).`);
42
142
  }
43
143
  }
44
144
  catch (err) {
45
- outputError(err.message, json);
46
- process.exit(1);
145
+ s?.stop();
146
+ handleError(err, json);
47
147
  }
48
148
  });
49
149
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerAnalyticsCommands(program: Command): void;
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerAnalyticsCommands(program: Command, createClient: ClientFactory): void;
@@ -1,35 +1,57 @@
1
- import { apiRequest } from "../api-client.js";
2
- import { outputJson, formatTable, outputError } from "../output.js";
3
- export function registerAnalyticsCommands(program) {
1
+ import { outputJson, formatTable, handleError, spinner, colors as c } from "../output.js";
2
+ export function registerAnalyticsCommands(program, createClient) {
4
3
  program
5
4
  .command("analytics")
6
5
  .description("FinOps analytics overview")
7
6
  .option("--days <n>", "Days to analyze", "30")
8
7
  .action(async (opts) => {
9
8
  const json = program.opts().json;
9
+ const s = json ? null : spinner("Loading analytics...").start();
10
10
  try {
11
- const data = await apiRequest(`/analytics/overview?days=${opts.days}`);
11
+ const client = createClient();
12
+ const data = await client.analytics.overview();
13
+ s?.stop();
12
14
  if (json) {
13
15
  outputJson(data);
14
16
  }
15
17
  else {
16
- console.log(`Analytics (last ${opts.days} days)\n`);
17
- if (data.dailyCosts) {
18
+ console.log(c.bold(`\nAnalytics Overview\n`));
19
+ // Summary stats
20
+ const totalSpend = data.totalSpendPeriod ?? 0;
21
+ const avgDaily = data.avgDailyCost ?? 0;
22
+ const projected = data.projectedMonthlyCost ?? 0;
23
+ const savings = data.savingsEstimate ?? 0;
24
+ const actions = data.killSwitchActions ?? 0;
25
+ console.log(` ${c.bold("Total spend:")} $${totalSpend.toFixed(2)}`);
26
+ console.log(` ${c.bold("Avg daily cost:")} $${avgDaily.toFixed(2)}`);
27
+ console.log(` ${c.bold("Projected monthly:")} $${projected.toFixed(2)}`);
28
+ console.log(` ${c.bold("Savings estimate:")} ${c.green("$" + savings.toFixed(2))}`);
29
+ console.log(` ${c.bold("Kill switch actions:")} ${actions > 0 ? c.yellow(String(actions)) : c.dim("0")}`);
30
+ // Last 7 days cost table
31
+ if (data.dailyCosts?.length) {
32
+ console.log(c.bold("\nLast 7 Days:\n"));
18
33
  formatTable(data.dailyCosts.slice(-7), [
19
- { key: "date", header: "Date" },
20
- { key: "totalUsd", header: "Cost (USD)" },
21
- { key: "violations", header: "Violations" },
22
- { key: "actions", header: "Actions" },
34
+ { key: "date", header: "Date", width: 12 },
35
+ { key: "cost", header: "Cost (USD)", width: 12 },
36
+ { key: "services", header: "Services", width: 10 },
37
+ { key: "violations", header: "Violations", width: 12 },
23
38
  ]);
24
39
  }
25
- if (data.totalSavingsUsd !== undefined) {
26
- console.log(`\nEstimated savings: $${data.totalSavingsUsd}`);
40
+ // Per-account breakdown
41
+ if (data.accountBreakdown?.length) {
42
+ console.log(c.bold("\nAccount Breakdown:\n"));
43
+ formatTable(data.accountBreakdown, [
44
+ { key: "provider", header: "Provider", width: 14 },
45
+ { key: "totalCost", header: "Total Cost", width: 12 },
46
+ { key: "avgDailyCost", header: "Avg Daily", width: 12 },
47
+ ]);
27
48
  }
49
+ console.log();
28
50
  }
29
51
  }
30
52
  catch (err) {
31
- outputError(err.message, json);
32
- process.exit(1);
53
+ s?.stop();
54
+ handleError(err, json);
33
55
  }
34
56
  });
35
57
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerAuthCommands(program: Command): void;
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerAuthCommands(program: Command, createClient: ClientFactory): void;
@@ -1,35 +1,41 @@
1
+ import { KillSwitchClient } from "@kill-switch/sdk";
1
2
  import { saveConfig, deleteConfig, resolveApiKey, resolveApiUrl } from "../config.js";
2
- import { apiRequest } from "../api-client.js";
3
- import { outputJson, formatObject, outputError } from "../output.js";
4
- import { createInterface } from "readline";
3
+ import { outputJson, formatObject, outputError, handleError, spinner, success } from "../output.js";
4
+ import { ask } from "../prompts.js";
5
5
  import { execFile } from "child_process";
6
- function ask(question) {
7
- const rl = createInterface({ input: process.stdin, output: process.stdout });
8
- return new Promise((resolve) => {
9
- rl.question(question, (answer) => {
10
- rl.close();
11
- resolve(answer.trim());
12
- });
13
- });
14
- }
6
+ import { CLI_VERSION } from "../version.js";
7
+ import { runDeviceFlow, defaultDeviceFlowDeps } from "../device-flow.js";
15
8
  function openBrowser(url) {
16
9
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
17
10
  execFile(cmd, [url], () => { });
18
11
  }
19
- export function registerAuthCommands(program) {
12
+ // Wires the device-flow helper to this command's spinner + console output.
13
+ // Kept here (not in device-flow.ts) so the helper stays free of CLI-specific
14
+ // presentation concerns and remains pure for unit testing.
15
+ function deviceFlowWithSpinner() {
16
+ let sp = null;
17
+ return defaultDeviceFlowDeps({
18
+ log: (line) => console.log(line),
19
+ spinnerStart: (label) => { sp = spinner(label).start(); },
20
+ spinnerStop: () => { sp?.stop(); sp = null; },
21
+ });
22
+ }
23
+ export function registerAuthCommands(program, createClient) {
20
24
  const auth = program.command("auth").description("Manage authentication");
21
25
  auth
22
26
  .command("setup")
23
- .description("Create an API key (opens browser to sign in, then paste the key)")
24
- .action(async () => {
27
+ .description("Authenticate via your browser (opens a one-time approval page)")
28
+ .option("--manual", "Manual flow: open Settings page, paste the key yourself")
29
+ .action(async (opts) => {
25
30
  const json = program.opts().json;
26
31
  const existing = resolveApiKey();
27
32
  if (existing) {
28
33
  try {
29
- const result = await apiRequest("/accounts/me");
34
+ const client = createClient();
35
+ const result = await client.account.me();
30
36
  if (!json) {
31
37
  console.log(`Already authenticated as ${result.name || result._id}.`);
32
- const proceed = await ask("Create a new API key anyway? (y/N): ");
38
+ const proceed = await ask("Re-authenticate anyway? (y/N): ");
33
39
  if (proceed.toLowerCase() !== "y")
34
40
  return;
35
41
  }
@@ -38,70 +44,103 @@ export function registerAuthCommands(program) {
38
44
  // Key invalid, proceed
39
45
  }
40
46
  }
41
- if (!json) {
42
- console.log("\n\u26a1 Kill Switch CLI Setup\n");
43
- console.log("Opening app.kill-switch.net in your browser...");
44
- console.log("1. Sign in (or create an account)");
45
- console.log("2. Go to Settings > API Keys");
46
- console.log("3. Click 'Create API Key'");
47
- console.log("4. Copy the key and paste it below\n");
48
- }
49
- openBrowser("https://app.kill-switch.net/settings");
50
- const key = await ask("Paste your API key (ks_live_...): ");
51
- if (!key.startsWith("ks_")) {
52
- outputError("API key must start with 'ks_'. Try again.", json);
53
- process.exit(1);
47
+ const apiUrl = resolveApiUrl();
48
+ // Manual flow \u2014 original copy/paste path for offline / scripted setups
49
+ if (opts.manual) {
50
+ if (!json) {
51
+ console.log("\n\u26a1 Kill Switch CLI Setup (manual)\n");
52
+ console.log("1. Sign in at app.kill-switch.net");
53
+ console.log("2. Go to Settings > API Keys");
54
+ console.log("3. Click 'Create API Key'");
55
+ console.log("4. Copy the key and paste it below\n");
56
+ }
57
+ openBrowser("https://app.kill-switch.net/settings");
58
+ const key = await ask("Paste your API key (ks_live_...): ");
59
+ if (!key.startsWith("ks_")) {
60
+ outputError("API key must start with 'ks_'.", json);
61
+ process.exit(1);
62
+ }
63
+ try {
64
+ const client = new KillSwitchClient({ apiKey: key, baseUrl: apiUrl });
65
+ const result = await client.account.me();
66
+ saveConfig({ apiKey: key, apiUrl });
67
+ if (json)
68
+ outputJson({ authenticated: true, account: result.name || result._id });
69
+ else {
70
+ success(`Authenticated as ${result.name || result._id}`);
71
+ console.log("API key saved to ~/.kill-switch/config.json");
72
+ }
73
+ }
74
+ catch (err) {
75
+ handleError(err, json);
76
+ }
77
+ return;
54
78
  }
79
+ // Device flow \u2014 default path
55
80
  try {
56
- const result = await apiRequest("/accounts/me", { apiKey: key });
57
- saveConfig({ apiKey: key, apiUrl: resolveApiUrl() });
81
+ const apiKey = await runDeviceFlow(apiUrl, CLI_VERSION, json, deviceFlowWithSpinner());
82
+ const client = new KillSwitchClient({ apiKey, baseUrl: apiUrl });
83
+ const result = await client.account.me();
84
+ saveConfig({ apiKey, apiUrl });
58
85
  if (json) {
59
86
  outputJson({ authenticated: true, account: result.name || result._id });
60
87
  }
61
88
  else {
62
- console.log(`\n\u2713 Authenticated as ${result.name || result._id}`);
89
+ success(`Authenticated as ${result.name || result._id}`);
63
90
  console.log("API key saved to ~/.kill-switch/config.json\n");
64
- console.log("Next: ks onboard --provider cloudflare --help-provider cloudflare");
91
+ console.log("Next: ks onboard --help-provider mongodb");
65
92
  }
66
93
  }
67
94
  catch (err) {
68
- outputError(`Authentication failed: ${err.message}`, json);
69
- process.exit(2);
95
+ handleError(err, json);
70
96
  }
71
97
  });
72
98
  auth
73
99
  .command("login")
74
- .description("Authenticate with an existing API key")
75
- .option("--api-key <key>", "Personal API key (starts with ks_)")
100
+ .description("Authenticate (browser device flow by default, or pass --api-key for direct)")
101
+ .option("--api-key <key>", "Personal API key (starts with ks_) — skips browser flow")
76
102
  .action(async (opts) => {
77
103
  const json = program.opts().json;
104
+ const apiUrl = resolveApiUrl();
78
105
  let key = opts.apiKey;
106
+ // Device flow when no --api-key. JSON mode requires --api-key (no browser).
79
107
  if (!key) {
80
108
  if (json) {
81
109
  outputError("--api-key is required in JSON mode", json);
82
110
  process.exit(1);
83
111
  }
84
- key = await ask("API key (ks_live_...): ");
112
+ try {
113
+ key = await runDeviceFlow(apiUrl, CLI_VERSION, json, deviceFlowWithSpinner());
114
+ }
115
+ catch (err) {
116
+ handleError(err, json);
117
+ return;
118
+ }
85
119
  }
86
120
  if (!key.startsWith("ks_")) {
87
121
  outputError("API key must start with 'ks_'. Create one at app.kill-switch.net or run: ks auth setup", json);
88
122
  process.exit(1);
89
123
  }
90
- // Validate the key by calling the API
124
+ const s = json ? null : spinner("Validating API key...").start();
91
125
  try {
92
- const result = await apiRequest("/accounts/me", { apiKey: key });
93
- saveConfig({ apiKey: key, apiUrl: resolveApiUrl() });
126
+ const client = new KillSwitchClient({
127
+ apiKey: key,
128
+ baseUrl: apiUrl,
129
+ });
130
+ const result = await client.account.me();
131
+ s?.stop();
132
+ saveConfig({ apiKey: key, apiUrl });
94
133
  if (json) {
95
134
  outputJson({ authenticated: true, account: result.name || result._id });
96
135
  }
97
136
  else {
98
- console.log(`Authenticated as ${result.name || result._id}`);
137
+ success(`Authenticated as ${result.name || result._id}`);
99
138
  console.log("API key saved to ~/.kill-switch/config.json");
100
139
  }
101
140
  }
102
141
  catch (err) {
103
- outputError(`Authentication failed: ${err.message}`, json);
104
- process.exit(2);
142
+ s?.stop();
143
+ handleError(err, json);
105
144
  }
106
145
  });
107
146
  auth
@@ -133,7 +172,8 @@ export function registerAuthCommands(program) {
133
172
  return;
134
173
  }
135
174
  try {
136
- const result = await apiRequest("/accounts/me");
175
+ const client = createClient();
176
+ const result = await client.account.me();
137
177
  if (json) {
138
178
  outputJson({ authenticated: true, ...result });
139
179
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerCheckCommands(program: Command): void;
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerCheckCommands(program: Command, createClient: ClientFactory): void;
@@ -1,37 +1,50 @@
1
- import { apiRequest } from "../api-client.js";
2
- import { outputJson, formatTable, outputError } from "../output.js";
3
- export function registerCheckCommands(program) {
1
+ import { outputJson, formatTable, handleError, spinner, colors as c } from "../output.js";
2
+ export function registerCheckCommands(program, createClient) {
4
3
  program
5
4
  .command("check")
6
5
  .description("Run monitoring check on all connected accounts")
7
6
  .action(async () => {
8
7
  const json = program.opts().json;
8
+ const s = json ? null : spinner("Running monitoring check...").start();
9
9
  try {
10
- const data = await apiRequest("/check", { method: "POST" });
10
+ const client = createClient();
11
+ const data = await client.monitoring.checkAll();
12
+ s?.stop();
11
13
  if (json) {
12
14
  outputJson(data);
13
15
  }
14
16
  else {
15
17
  const results = data.results || [];
16
- console.log(`Checked ${results.length} account(s)\n`);
18
+ const totalViolations = results.reduce((n, r) => n + (r.violations?.length || 0), 0);
19
+ console.log(`Checked ${c.bold(String(results.length))} account(s) — ${totalViolations === 0 ? c.green("all clear") : c.red(totalViolations + " violation(s)")}\n`);
17
20
  for (const r of results) {
18
- console.log(`${r.provider || "unknown"}: ${r.name || r.cloudAccountId}`);
21
+ const statusMark = r.status === "violation" ? c.red("✗") : r.status === "error" ? c.yellow("⚠") : c.green("✓");
22
+ console.log(`${statusMark} ${c.bold((r.provider || "unknown").padEnd(12))} ${r.name || r.cloudAccountId}`);
19
23
  if (r.violations?.length) {
20
- formatTable(r.violations, [
21
- { key: "metric", header: "Metric" },
22
- { key: "value", header: "Value" },
23
- { key: "threshold", header: "Threshold" },
24
+ // Compute multiplier for each violation inline
25
+ const rows = r.violations.map((v) => ({
26
+ ...v,
27
+ multiplier: v.threshold > 0 ? `${Math.round(v.currentValue / v.threshold)}x` : "",
28
+ }));
29
+ formatTable(rows, [
30
+ { key: "serviceName", header: "Service", width: 28 },
31
+ { key: "metricName", header: "Metric", width: 22 },
32
+ { key: "currentValue", header: "Current", width: 14 },
33
+ { key: "threshold", header: "Threshold", width: 12 },
34
+ { key: "multiplier", header: "Over", width: 8 },
35
+ { key: "severity", header: "Severity", width: 10 },
24
36
  ]);
37
+ if (r.actionsTaken?.length) {
38
+ console.log(c.dim(` Actions: ${r.actionsTaken.join(", ")}`));
39
+ }
25
40
  }
26
- else {
27
- console.log(" All clear\n");
28
- }
41
+ console.log();
29
42
  }
30
43
  }
31
44
  }
32
45
  catch (err) {
33
- outputError(err.message, json);
34
- process.exit(1);
46
+ s?.stop();
47
+ handleError(err, json);
35
48
  }
36
49
  });
37
50
  }
@@ -1,5 +1,16 @@
1
1
  import { loadConfig, saveConfig, CONFIG_FILE, DEFAULT_API_URL } from "../config.js";
2
2
  import { outputJson } from "../output.js";
3
+ /** Mask a secret so it never lands in logs / CI output in full. */
4
+ function maskKey(v) {
5
+ const s = String(v ?? "");
6
+ return s.length > 12 ? `${s.slice(0, 12)}…${s.slice(-2)}` : "****";
7
+ }
8
+ /** Return a copy of the config with secret values masked unless `reveal`. */
9
+ function redactConfig(cfg, reveal) {
10
+ if (reveal || !cfg.apiKey)
11
+ return cfg;
12
+ return { ...cfg, apiKey: maskKey(cfg.apiKey) };
13
+ }
3
14
  export function registerConfigCommands(program) {
4
15
  const config = program.command("config").description("Manage CLI configuration");
5
16
  config
@@ -19,10 +30,13 @@ export function registerConfigCommands(program) {
19
30
  config
20
31
  .command("get <key>")
21
32
  .description("Get a config value")
22
- .action((key) => {
33
+ .option("--reveal", "Show secret values (e.g. apiKey) in full")
34
+ .action((key, opts) => {
23
35
  const json = program.opts().json;
24
36
  const cfg = loadConfig();
25
- const value = cfg[key];
37
+ let value = cfg[key];
38
+ if (key === "apiKey" && value && !opts.reveal)
39
+ value = maskKey(value);
26
40
  if (json) {
27
41
  outputJson({ [key]: value ?? null });
28
42
  }
@@ -49,11 +63,12 @@ export function registerConfigCommands(program) {
49
63
  .command("list")
50
64
  .alias("ls")
51
65
  .description("Show all config values")
52
- .action(() => {
66
+ .option("--reveal", "Show secret values (e.g. apiKey) in full")
67
+ .action((opts) => {
53
68
  const json = program.opts().json;
54
69
  const cfg = loadConfig();
55
70
  if (json) {
56
- outputJson(cfg);
71
+ outputJson(redactConfig(cfg, !!opts?.reveal));
57
72
  }
58
73
  else {
59
74
  const entries = Object.entries(cfg);
@@ -62,7 +77,7 @@ export function registerConfigCommands(program) {
62
77
  }
63
78
  else {
64
79
  for (const [k, v] of entries) {
65
- const display = k === "apiKey" ? String(v).substring(0, 16) + "..." : String(v);
80
+ const display = k === "apiKey" && !opts?.reveal ? maskKey(v) : String(v);
66
81
  console.log(`${k.padEnd(12)} ${display}`);
67
82
  }
68
83
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerKillCommands(program: Command): void;
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerKillCommands(program: Command, createClient: ClientFactory): void;