@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
@@ -0,0 +1,192 @@
1
+ import { outputJson, formatTable, formatObject, handleError, success } from "../output.js";
2
+ import { confirm } from "../prompts.js";
3
+ export function registerOrgCommands(program, createClient) {
4
+ const orgs = program.command("orgs").description("Manage organizations");
5
+ orgs
6
+ .command("list")
7
+ .alias("ls")
8
+ .description("List organizations you belong to")
9
+ .action(async () => {
10
+ const json = program.opts().json;
11
+ try {
12
+ const client = createClient();
13
+ const data = await client.orgs.list();
14
+ if (json) {
15
+ outputJson(data);
16
+ }
17
+ else {
18
+ console.log(`Active org: ${data.activeOrgId || "none"}\n`);
19
+ formatTable(data.orgs, [
20
+ { key: "id", header: "ID" },
21
+ { key: "name", header: "Name" },
22
+ { key: "type", header: "Type" },
23
+ { key: "tier", header: "Tier" },
24
+ { key: "role", header: "Role" },
25
+ { key: "slug", header: "Slug" },
26
+ ]);
27
+ }
28
+ }
29
+ catch (err) {
30
+ handleError(err, json);
31
+ }
32
+ });
33
+ orgs
34
+ .command("create <name>")
35
+ .description("Create a new organization (requires team/enterprise tier)")
36
+ .action(async (name) => {
37
+ const json = program.opts().json;
38
+ try {
39
+ const client = createClient();
40
+ const data = await client.orgs.create({ name });
41
+ if (json) {
42
+ outputJson(data);
43
+ }
44
+ else {
45
+ console.log(`Organization created:`);
46
+ console.log(` ID: ${data.id}`);
47
+ console.log(` Name: ${data.name}`);
48
+ console.log(` Slug: ${data.slug}`);
49
+ console.log(` Type: ${data.type}`);
50
+ console.log(`\nSwitch to it with: ks orgs switch ${data.id}`);
51
+ }
52
+ }
53
+ catch (err) {
54
+ handleError(err, json);
55
+ }
56
+ });
57
+ orgs
58
+ .command("switch <orgId>")
59
+ .description("Switch active organization")
60
+ .action(async (orgId) => {
61
+ const json = program.opts().json;
62
+ try {
63
+ const client = createClient();
64
+ await client.orgs.switch(orgId);
65
+ if (json) {
66
+ outputJson({ switched: true, activeOrgId: orgId });
67
+ }
68
+ else {
69
+ console.log(`Switched to org: ${orgId}`);
70
+ }
71
+ }
72
+ catch (err) {
73
+ handleError(err, json);
74
+ }
75
+ });
76
+ orgs
77
+ .command("info [orgId]")
78
+ .description("Get organization details")
79
+ .action(async (orgId) => {
80
+ const json = program.opts().json;
81
+ try {
82
+ const client = createClient();
83
+ if (orgId) {
84
+ const data = await client.orgs.get(orgId);
85
+ if (json) {
86
+ outputJson(data);
87
+ }
88
+ else {
89
+ formatObject(data);
90
+ }
91
+ }
92
+ else {
93
+ // Show current org info via /accounts/me
94
+ const data = await client.account.me();
95
+ if (json) {
96
+ outputJson(data);
97
+ }
98
+ else {
99
+ formatObject({
100
+ name: data.name,
101
+ tier: data.tier,
102
+ type: data.type || "personal",
103
+ slug: data.slug,
104
+ role: data.teamRole,
105
+ activeOrgId: data.activeOrgId,
106
+ orgCount: data.orgs?.length || 1,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ catch (err) {
112
+ handleError(err, json);
113
+ }
114
+ });
115
+ orgs
116
+ .command("members")
117
+ .description("List team members in current organization")
118
+ .action(async () => {
119
+ const json = program.opts().json;
120
+ try {
121
+ const client = createClient();
122
+ const data = await client.teams.members();
123
+ if (json) {
124
+ outputJson(data);
125
+ }
126
+ else {
127
+ console.log("Team Members:");
128
+ formatTable(data.members || [], [
129
+ { key: "email", header: "Email" },
130
+ { key: "role", header: "Role" },
131
+ { key: "isOwner", header: "Owner" },
132
+ ]);
133
+ if (data.invitations?.length) {
134
+ console.log("\nPending Invitations:");
135
+ formatTable(data.invitations, [
136
+ { key: "email", header: "Email" },
137
+ { key: "role", header: "Role" },
138
+ { key: "status", header: "Status" },
139
+ ]);
140
+ }
141
+ }
142
+ }
143
+ catch (err) {
144
+ handleError(err, json);
145
+ }
146
+ });
147
+ orgs
148
+ .command("invite <email>")
149
+ .description("Invite a member to the current organization")
150
+ .option("--role <role>", "Role: admin, member, or viewer", "member")
151
+ .action(async (email, opts) => {
152
+ const json = program.opts().json;
153
+ try {
154
+ const client = createClient();
155
+ const data = await client.teams.invite({ email, role: opts.role });
156
+ if (json) {
157
+ outputJson(data);
158
+ }
159
+ else {
160
+ console.log(`Invitation sent to ${email} (role: ${opts.role})`);
161
+ console.log(`Accept URL: ${data.acceptUrl}`);
162
+ }
163
+ }
164
+ catch (err) {
165
+ handleError(err, json);
166
+ }
167
+ });
168
+ orgs
169
+ .command("delete <orgId>")
170
+ .description("Delete an organization (owner only, cannot delete personal workspace)")
171
+ .action(async (orgId) => {
172
+ const { json, yes } = program.opts();
173
+ try {
174
+ const ok = await confirm(`Delete organization ${orgId}? This cannot be undone.`, { yes, json });
175
+ if (!ok) {
176
+ console.log("Aborted.");
177
+ return;
178
+ }
179
+ const client = createClient();
180
+ await client.orgs.delete(orgId);
181
+ if (json) {
182
+ outputJson({ deleted: true, orgId });
183
+ }
184
+ else {
185
+ success(`Organization ${orgId} deleted.`);
186
+ }
187
+ }
188
+ catch (err) {
189
+ handleError(err, json);
190
+ }
191
+ });
192
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerProviderCommands(program: Command, createClient: ClientFactory): void;
@@ -0,0 +1,82 @@
1
+ import { outputJson, formatTable, handleError, spinner, success, fail, colors as c } from "../output.js";
2
+ export function registerProviderCommands(program, createClient) {
3
+ const providers = program.command("providers").description("Cloud provider information and credential validation");
4
+ providers
5
+ .command("list")
6
+ .alias("ls")
7
+ .description("List supported cloud providers")
8
+ .action(async () => {
9
+ const json = program.opts().json;
10
+ try {
11
+ const client = createClient();
12
+ const list = await client.providers.list();
13
+ if (json) {
14
+ outputJson(list);
15
+ }
16
+ else {
17
+ formatTable(list, [
18
+ { key: "id", header: "ID", width: 14 },
19
+ { key: "name", header: "Name", width: 25 },
20
+ ]);
21
+ }
22
+ }
23
+ catch (err) {
24
+ handleError(err, json);
25
+ }
26
+ });
27
+ providers
28
+ .command("validate <provider>")
29
+ .description("Validate cloud provider credentials without connecting")
30
+ .option("--token <token>", "API token (Cloudflare)")
31
+ .option("--account-id <id>", "Account ID (Cloudflare)")
32
+ .option("--project-id <id>", "Project ID (GCP)")
33
+ .option("--service-account <json>", "Service Account JSON (GCP)")
34
+ .option("--access-key <key>", "Access Key ID (AWS)")
35
+ .option("--secret-key <key>", "Secret Access Key (AWS)")
36
+ .option("--region <region>", "Region (AWS)")
37
+ .option("--runpod-api-key <key>", "API Key (RunPod)")
38
+ .action(async (provider, opts) => {
39
+ const json = program.opts().json;
40
+ const credential = {};
41
+ // Build credential based on provider
42
+ if (opts.token)
43
+ credential.apiToken = opts.token;
44
+ if (opts.accountId)
45
+ credential.accountId = opts.accountId;
46
+ if (opts.projectId)
47
+ credential.projectId = opts.projectId;
48
+ if (opts.serviceAccount)
49
+ credential.serviceAccountJson = opts.serviceAccount;
50
+ if (opts.accessKey)
51
+ credential.awsAccessKeyId = opts.accessKey;
52
+ if (opts.secretKey)
53
+ credential.awsSecretAccessKey = opts.secretKey;
54
+ if (opts.region)
55
+ credential.awsRegion = opts.region;
56
+ if (opts.runpodApiKey)
57
+ credential.runpodApiKey = opts.runpodApiKey;
58
+ const s = json ? null : spinner(`Validating ${provider} credentials...`).start();
59
+ try {
60
+ const client = createClient();
61
+ const result = await client.providers.validate(provider, credential);
62
+ s?.stop();
63
+ if (json) {
64
+ outputJson(result);
65
+ }
66
+ else if (result.valid) {
67
+ success("Credentials are valid!");
68
+ if (result.accountId)
69
+ console.log(` ${c.bold("Account ID:")} ${result.accountId}`);
70
+ if (result.accountName)
71
+ console.log(` ${c.bold("Account Name:")} ${result.accountName}`);
72
+ }
73
+ else {
74
+ fail(`Validation failed: ${result.error || "Unknown error"}`);
75
+ }
76
+ }
77
+ catch (err) {
78
+ s?.stop();
79
+ handleError(err, json);
80
+ }
81
+ });
82
+ }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerRuleCommands(program: Command): void;
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerRuleCommands(program: Command, createClient: ClientFactory): void;
@@ -1,21 +1,30 @@
1
- import { apiRequest } from "../api-client.js";
2
- import { outputJson, formatTable, outputError } from "../output.js";
3
- export function registerRuleCommands(program) {
1
+ import { outputJson, formatTable, handleError, success, colors as c } from "../output.js";
2
+ import { confirm } from "../prompts.js";
3
+ export function registerRuleCommands(program, createClient) {
4
4
  const rules = program.command("rules").description("Manage kill switch rules");
5
5
  rules
6
6
  .command("list")
7
7
  .alias("ls")
8
8
  .description("List active rules")
9
- .action(async () => {
9
+ .option("--trigger <type>", "Filter by trigger type (cost, security, custom, api, agent)")
10
+ .option("--enabled", "Show only enabled rules")
11
+ .option("--disabled", "Show only disabled rules")
12
+ .action(async (opts) => {
10
13
  const json = program.opts().json;
11
14
  try {
12
- const data = await apiRequest("/rules");
13
- const list = data.rules || data;
15
+ const client = createClient();
16
+ let list = await client.rules.list();
17
+ if (opts.trigger)
18
+ list = list.filter((r) => r.trigger === opts.trigger);
19
+ if (opts.enabled)
20
+ list = list.filter((r) => r.enabled);
21
+ if (opts.disabled)
22
+ list = list.filter((r) => !r.enabled);
14
23
  if (json) {
15
24
  outputJson(list);
16
25
  }
17
26
  else {
18
- formatTable(Array.isArray(list) ? list : [], [
27
+ formatTable(list, [
19
28
  { key: "id", header: "ID" },
20
29
  { key: "name", header: "Name" },
21
30
  { key: "trigger", header: "Trigger" },
@@ -24,8 +33,7 @@ export function registerRuleCommands(program) {
24
33
  }
25
34
  }
26
35
  catch (err) {
27
- outputError(err.message, json);
28
- process.exit(1);
36
+ handleError(err, json);
29
37
  }
30
38
  });
31
39
  rules
@@ -34,8 +42,8 @@ export function registerRuleCommands(program) {
34
42
  .action(async () => {
35
43
  const json = program.opts().json;
36
44
  try {
37
- const data = await apiRequest("/rules/presets", { public: true });
38
- const presets = data.presets || data;
45
+ const client = createClient();
46
+ const presets = await client.rules.presets();
39
47
  if (json) {
40
48
  outputJson(presets);
41
49
  }
@@ -48,8 +56,7 @@ export function registerRuleCommands(program) {
48
56
  }
49
57
  }
50
58
  catch (err) {
51
- outputError(err.message, json);
52
- process.exit(1);
59
+ handleError(err, json);
53
60
  }
54
61
  });
55
62
  rules
@@ -58,6 +65,7 @@ export function registerRuleCommands(program) {
58
65
  .requiredOption("--trigger <type>", "Trigger type (cost, security, api)")
59
66
  .option("--condition <json>", "Condition JSON")
60
67
  .option("--action <json>", "Action JSON")
68
+ .option("--dry-run", "Preview rule without creating it")
61
69
  .action(async (name, opts) => {
62
70
  const json = program.opts().json;
63
71
  try {
@@ -66,17 +74,27 @@ export function registerRuleCommands(program) {
66
74
  body.conditions = JSON.parse(opts.condition);
67
75
  if (opts.action)
68
76
  body.actions = JSON.parse(opts.action);
69
- const data = await apiRequest("/rules", { method: "POST", body });
77
+ if (opts.dryRun) {
78
+ if (json) {
79
+ outputJson({ dryRun: true, rule: body });
80
+ }
81
+ else {
82
+ console.log(c.yellow("Dry run — rule would be created:"));
83
+ console.log(JSON.stringify(body, null, 2));
84
+ }
85
+ return;
86
+ }
87
+ const client = createClient();
88
+ const rule = await client.rules.create(body);
70
89
  if (json) {
71
- outputJson(data);
90
+ outputJson(rule);
72
91
  }
73
92
  else {
74
- console.log(`Rule created: ${data.id || data._id}`);
93
+ success(`Rule created: ${rule.id}`);
75
94
  }
76
95
  }
77
96
  catch (err) {
78
- outputError(err.message, json);
79
- process.exit(1);
97
+ handleError(err, json);
80
98
  }
81
99
  });
82
100
  rules
@@ -84,19 +102,24 @@ export function registerRuleCommands(program) {
84
102
  .alias("rm")
85
103
  .description("Delete a rule")
86
104
  .action(async (id) => {
87
- const json = program.opts().json;
105
+ const { json, yes } = program.opts();
88
106
  try {
89
- await apiRequest(`/rules/${id}`, { method: "DELETE" });
107
+ const ok = await confirm(`Delete rule ${id}?`, { yes, json });
108
+ if (!ok) {
109
+ console.log("Aborted.");
110
+ return;
111
+ }
112
+ const client = createClient();
113
+ await client.rules.delete(id);
90
114
  if (json) {
91
115
  outputJson({ deleted: true, id });
92
116
  }
93
117
  else {
94
- console.log(`Rule ${id} deleted.`);
118
+ success(`Rule ${id} deleted.`);
95
119
  }
96
120
  }
97
121
  catch (err) {
98
- outputError(err.message, json);
99
- process.exit(1);
122
+ handleError(err, json);
100
123
  }
101
124
  });
102
125
  rules
@@ -105,17 +128,17 @@ export function registerRuleCommands(program) {
105
128
  .action(async (id) => {
106
129
  const json = program.opts().json;
107
130
  try {
108
- const data = await apiRequest(`/rules/${id}/toggle`, { method: "POST" });
131
+ const client = createClient();
132
+ const rule = await client.rules.toggle(id);
109
133
  if (json) {
110
- outputJson(data);
134
+ outputJson(rule);
111
135
  }
112
136
  else {
113
- console.log(`Rule ${id} is now ${data.enabled ? "enabled" : "disabled"}.`);
137
+ success(`Rule ${id} is now ${rule.enabled ? c.green("enabled") : c.yellow("disabled")}.`);
114
138
  }
115
139
  }
116
140
  catch (err) {
117
- outputError(err.message, json);
118
- process.exit(1);
141
+ handleError(err, json);
119
142
  }
120
143
  });
121
144
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerShieldCommands(program: Command): void;
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerShieldCommands(program: Command, createClient: ClientFactory): void;
@@ -1,20 +1,20 @@
1
- import { apiRequest } from "../api-client.js";
2
- import { outputJson, formatTable, outputError } from "../output.js";
1
+ import { outputJson, formatTable, handleError, spinner, success, colors as c } from "../output.js";
3
2
  const PRESETS = [
4
3
  "ddos", "brute-force", "cost-runaway", "error-storm",
5
4
  "exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
6
5
  ];
7
- export function registerShieldCommands(program) {
8
- const shield = program
6
+ export function registerShieldCommands(program, createClient) {
7
+ program
9
8
  .command("shield [preset]")
10
9
  .description("Quick-apply a protection preset (e.g., kill-switch shield cost-runaway)")
11
10
  .option("--list", "List available shields")
11
+ .option("--dry-run", "Preview what the shield would do without applying")
12
12
  .action(async (preset, opts) => {
13
13
  const json = program.opts().json;
14
+ const client = createClient();
14
15
  if (opts.list || !preset) {
15
16
  try {
16
- const data = await apiRequest("/rules/presets", { public: true });
17
- const presets = data.presets || data;
17
+ const presets = await client.rules.presets();
18
18
  if (json) {
19
19
  outputJson(presets);
20
20
  }
@@ -29,29 +29,48 @@ export function registerShieldCommands(program) {
29
29
  }
30
30
  }
31
31
  catch (err) {
32
- outputError(err.message, json);
33
- process.exit(1);
32
+ handleError(err, json);
34
33
  }
35
34
  return;
36
35
  }
37
36
  if (!PRESETS.includes(preset)) {
38
- outputError(`Unknown preset "${preset}". Run: kill-switch shield --list`, json);
39
- process.exit(1);
37
+ handleError(new Error(`Unknown preset "${preset}". Run: kill-switch shield --list`), json);
40
38
  }
39
+ if (opts.dryRun) {
40
+ try {
41
+ const presets = await client.rules.presets();
42
+ const match = presets.find((p) => p.id === preset);
43
+ if (json) {
44
+ outputJson({ dryRun: true, preset, details: match });
45
+ }
46
+ else {
47
+ console.log(c.yellow("Dry run — shield would be applied:"));
48
+ console.log(` ${c.bold("Shield:")} ${match?.name || preset}`);
49
+ console.log(` ${c.bold("Description:")} ${match?.description || "N/A"}`);
50
+ console.log(` ${c.bold("Category:")} ${match?.category || "N/A"}`);
51
+ }
52
+ }
53
+ catch (err) {
54
+ handleError(err, json);
55
+ }
56
+ return;
57
+ }
58
+ const s = json ? null : spinner(`Applying ${preset} shield...`).start();
41
59
  try {
42
- const data = await apiRequest(`/rules/presets/${preset}`, { method: "POST" });
60
+ const rule = await client.rules.applyPreset(preset);
61
+ s?.stop();
43
62
  if (json) {
44
- outputJson(data);
63
+ outputJson(rule);
45
64
  }
46
65
  else {
47
- console.log(`Shield activated: ${data.name || preset}`);
48
- if (data.id)
49
- console.log(`Rule ID: ${data.id}`);
66
+ success(`Shield activated: ${rule.name || preset}`);
67
+ if (rule.id)
68
+ console.log(` Rule ID: ${c.dim(rule.id)}`);
50
69
  }
51
70
  }
52
71
  catch (err) {
53
- outputError(err.message, json);
54
- process.exit(1);
72
+ s?.stop();
73
+ handleError(err, json);
55
74
  }
56
75
  });
57
76
  }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerStatusCommand(program: Command, createClient: ClientFactory): void;
@@ -0,0 +1,100 @@
1
+ import { outputJson, formatTable, handleError, spinner, warn, colors as c } from "../output.js";
2
+ export function registerStatusCommand(program, createClient) {
3
+ program
4
+ .command("status")
5
+ .description("Show a quick overview of your Kill Switch setup")
6
+ .action(async () => {
7
+ const json = program.opts().json;
8
+ const s = json ? null : spinner("Loading status...").start();
9
+ try {
10
+ const client = createClient();
11
+ // Fire all requests in parallel
12
+ const [accountInfo, accounts, rules, sequences, channels, analytics] = await Promise.all([
13
+ client.billing.status().catch(() => null),
14
+ client.accounts.list().catch(() => []),
15
+ client.rules.list().catch(() => []),
16
+ client.database.list().catch(() => []),
17
+ client.alerts.channels().catch(() => []),
18
+ client.analytics.overview().catch(() => null),
19
+ ]);
20
+ s?.stop();
21
+ if (json) {
22
+ outputJson({
23
+ tier: accountInfo?.tier || "unknown",
24
+ limits: accountInfo?.limits,
25
+ accounts: accounts.length,
26
+ rules: rules.length,
27
+ activeKillSequences: sequences.length,
28
+ alertChannels: channels.length,
29
+ totalSpendPeriod: analytics?.totalSpendPeriod ?? 0,
30
+ savingsEstimate: analytics?.savingsEstimate ?? 0,
31
+ killSwitchActions: analytics?.killSwitchActions ?? 0,
32
+ });
33
+ return;
34
+ }
35
+ // Header
36
+ console.log(c.bold("\nKill Switch Status\n"));
37
+ // Tier
38
+ const tier = accountInfo?.tier || "unknown";
39
+ const tierColor = tier === "free" ? c.dim : tier === "pro" ? c.cyan : c.green;
40
+ console.log(` ${c.bold("Plan:")} ${tierColor(tier)}`);
41
+ // Accounts
42
+ const activeAccounts = accounts.filter((a) => a.status === "active").length;
43
+ const limit = accountInfo?.limits?.cloudAccounts;
44
+ console.log(` ${c.bold("Accounts:")} ${activeAccounts} active${limit ? c.dim(` / ${limit} max`) : ""}`);
45
+ // Rules (programmatic kill rules — account-level thresholds are set per account)
46
+ const enabledRules = rules.filter((r) => r.enabled).length;
47
+ console.log(` ${c.bold("Kill rules:")} ${enabledRules} enabled${rules.length > enabledRules ? c.dim(` (${rules.length} total)`) : ""}${enabledRules === 0 ? c.dim(" · thresholds set per account") : ""}`);
48
+ // Kill sequences
49
+ if (sequences.length > 0) {
50
+ warn(` ${c.bold("Kill sequences:")} ${sequences.length} active`);
51
+ }
52
+ else {
53
+ console.log(` ${c.bold("Kill seqs:")} ${c.dim("none")}`);
54
+ }
55
+ // Alert channels
56
+ if (channels.length > 0) {
57
+ const enabledChannels = channels.filter((ch) => ch.enabled !== false).length;
58
+ console.log(` ${c.bold("Alerts:")} ${enabledChannels} channel(s) — ${channels.map((ch) => ch.type).join(", ")}`);
59
+ }
60
+ else {
61
+ console.log(` ${c.bold("Alerts:")} ${c.yellow("none")} — run: ks alerts add --type pagerduty --routing-key KEY`);
62
+ }
63
+ // 30-day spend summary
64
+ if (analytics) {
65
+ const totalSpend = analytics.totalSpendPeriod ?? 0;
66
+ const savings = analytics.savingsEstimate ?? 0;
67
+ const actions = analytics.killSwitchActions ?? 0;
68
+ console.log(` ${c.bold("Spend (30d):")} $${totalSpend.toFixed(2)}${savings > 0 ? c.green(` · $${savings.toFixed(2)} saved`) : ""}${actions > 0 ? c.yellow(` · ${actions} action(s) taken`) : ""}`);
69
+ }
70
+ // Connected accounts table
71
+ if (accounts.length > 0) {
72
+ console.log(c.bold("\nConnected Accounts:\n"));
73
+ formatTable(accounts, [
74
+ { key: "provider", header: "Provider", width: 12 },
75
+ { key: "name", header: "Name", width: 25 },
76
+ { key: "status", header: "Status", width: 12 },
77
+ { key: "lastCheckStatus", header: "Last Check", width: 12 },
78
+ ]);
79
+ }
80
+ else {
81
+ console.log(`\n ${c.dim("No accounts connected. Run:")} ks onboard`);
82
+ }
83
+ // Alert channels detail
84
+ if (channels.length > 0) {
85
+ console.log(c.bold("\nAlert Channels:\n"));
86
+ formatTable(channels, [
87
+ { key: "type", header: "Type", width: 14 },
88
+ { key: "name", header: "Name", width: 20 },
89
+ { key: "enabled", header: "Enabled", width: 8 },
90
+ { key: "configPreview", header: "Config", width: 30 },
91
+ ]);
92
+ }
93
+ console.log();
94
+ }
95
+ catch (err) {
96
+ s?.stop();
97
+ handleError(err, json);
98
+ }
99
+ });
100
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerWatchCommand(program: Command, createClient: ClientFactory): void;