@kill-switch/cli 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) 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 +127 -14
  14. package/dist/commands/check.d.ts +2 -1
  15. package/dist/commands/check.js +28 -15
  16. package/dist/commands/kill.d.ts +2 -1
  17. package/dist/commands/kill.js +52 -37
  18. package/dist/commands/onboard.d.ts +2 -17
  19. package/dist/commands/onboard.js +273 -61
  20. package/dist/commands/orgs.d.ts +3 -0
  21. package/dist/commands/orgs.js +192 -0
  22. package/dist/commands/providers.d.ts +3 -0
  23. package/dist/commands/providers.js +82 -0
  24. package/dist/commands/rules.d.ts +2 -1
  25. package/dist/commands/rules.js +51 -28
  26. package/dist/commands/shield.d.ts +2 -1
  27. package/dist/commands/shield.js +36 -17
  28. package/dist/commands/status.d.ts +3 -0
  29. package/dist/commands/status.js +100 -0
  30. package/dist/commands/watch.d.ts +3 -0
  31. package/dist/commands/watch.js +68 -0
  32. package/dist/device-flow.d.ts +33 -0
  33. package/dist/device-flow.js +91 -0
  34. package/dist/index.js +38 -11
  35. package/dist/output.d.ts +26 -4
  36. package/dist/output.js +101 -12
  37. package/dist/prompts.d.ts +13 -0
  38. package/dist/prompts.js +24 -0
  39. package/dist/types.d.ts +2 -0
  40. package/dist/types.js +1 -0
  41. package/dist/version.d.ts +1 -0
  42. package/dist/version.js +4 -0
  43. package/package.json +29 -7
@@ -1,6 +1,6 @@
1
- import { apiRequest } from "../api-client.js";
2
- import { outputJson, formatTable, formatObject, outputError } from "../output.js";
3
- export function registerKillCommands(program) {
1
+ import { outputJson, formatTable, formatObject, handleError, spinner, success, warn, colors as c } from "../output.js";
2
+ import { confirm } from "../prompts.js";
3
+ export function registerKillCommands(program, createClient) {
4
4
  const kill = program.command("kill").description("Database kill switch sequences");
5
5
  kill
6
6
  .command("init")
@@ -8,25 +8,32 @@ export function registerKillCommands(program) {
8
8
  .requiredOption("--credential-id <id>", "Stored credential ID")
9
9
  .requiredOption("--trigger <reason>", "Kill trigger reason")
10
10
  .action(async (opts) => {
11
- const json = program.opts().json;
11
+ const { json, yes } = program.opts();
12
12
  try {
13
- const data = await apiRequest("/database/kill", {
14
- method: "POST",
15
- body: { credentialId: opts.credentialId, trigger: opts.trigger },
13
+ const ok = await confirm(`${c.red("WARNING:")} This will start a database kill sequence. Continue?`, { yes, json });
14
+ if (!ok) {
15
+ console.log("Aborted.");
16
+ return;
17
+ }
18
+ const s = json ? null : spinner("Initiating kill sequence...").start();
19
+ const client = createClient();
20
+ const data = await client.database.initiate({
21
+ credentialId: opts.credentialId,
22
+ trigger: opts.trigger,
16
23
  });
24
+ s?.stop();
17
25
  if (json) {
18
26
  outputJson(data);
19
27
  }
20
28
  else {
21
- console.log(`Kill sequence initiated: ${data.sequenceId}`);
22
- console.log(`Status: ${data.status}`);
23
- console.log(`Steps: ${data.steps?.map((s) => s.action).join(" -> ")}`);
24
- console.log(`\nAdvance: kill-switch kill advance ${data.sequenceId} --credential-id ${opts.credentialId}`);
29
+ warn(`Kill sequence initiated: ${c.bold(data.id)}`);
30
+ console.log(` Status: ${data.status}`);
31
+ console.log(` Steps: ${data.steps?.map((s) => s.action).join(" ")}`);
32
+ console.log(`\n Advance: ${c.dim(`ks kill advance ${data.id} --credential-id ${opts.credentialId}`)}`);
25
33
  }
26
34
  }
27
35
  catch (err) {
28
- outputError(err.message, json);
29
- process.exit(1);
36
+ handleError(err, json);
30
37
  }
31
38
  });
32
39
  kill
@@ -35,8 +42,9 @@ export function registerKillCommands(program) {
35
42
  .action(async (id) => {
36
43
  const json = program.opts().json;
37
44
  try {
45
+ const client = createClient();
38
46
  if (id) {
39
- const data = await apiRequest(`/database/kill/${id}`);
47
+ const data = await client.database.get(id);
40
48
  if (json) {
41
49
  outputJson(data);
42
50
  }
@@ -53,13 +61,12 @@ export function registerKillCommands(program) {
53
61
  }
54
62
  }
55
63
  else {
56
- const data = await apiRequest("/database/kill");
57
- const sequences = data.sequences || data;
64
+ const sequences = await client.database.list();
58
65
  if (json) {
59
66
  outputJson(sequences);
60
67
  }
61
68
  else {
62
- formatTable(Array.isArray(sequences) ? sequences : [], [
69
+ formatTable(sequences, [
63
70
  { key: "id", header: "Sequence ID" },
64
71
  { key: "status", header: "Status" },
65
72
  { key: "currentStep", header: "Step" },
@@ -68,8 +75,7 @@ export function registerKillCommands(program) {
68
75
  }
69
76
  }
70
77
  catch (err) {
71
- outputError(err.message, json);
72
- process.exit(1);
78
+ handleError(err, json);
73
79
  }
74
80
  });
75
81
  kill
@@ -78,48 +84,57 @@ export function registerKillCommands(program) {
78
84
  .requiredOption("--credential-id <credId>", "Stored credential ID")
79
85
  .option("--human-approval", "Confirm human approval (required for nuke step)")
80
86
  .action(async (id, opts) => {
81
- const json = program.opts().json;
87
+ const { json, yes } = program.opts();
82
88
  try {
83
- const data = await apiRequest(`/database/kill/${id}/advance`, {
84
- method: "POST",
85
- body: {
86
- credentialId: opts.credentialId,
87
- humanApproval: opts.humanApproval || false,
88
- },
89
+ const ok = await confirm(`Execute the next step in kill sequence ${id}?`, { yes, json });
90
+ if (!ok) {
91
+ console.log("Aborted.");
92
+ return;
93
+ }
94
+ const s = json ? null : spinner("Executing step...").start();
95
+ const client = createClient();
96
+ const data = await client.database.advance(id, {
97
+ credentialId: opts.credentialId,
98
+ humanApproval: opts.humanApproval || false,
89
99
  });
100
+ s?.stop();
90
101
  if (json) {
91
102
  outputJson(data);
92
103
  }
93
104
  else {
94
- console.log(`Step executed: ${data.steps?.[data.currentStep - 1]?.action || "?"}`);
95
- console.log(`Status: ${data.status}`);
105
+ success(`Step executed: ${data.steps?.[data.currentStep - 1]?.action || "?"}`);
106
+ console.log(` Status: ${data.status}`);
96
107
  if (data.status !== "completed") {
97
- console.log(`Next: kill-switch kill advance ${id} --credential-id ${opts.credentialId}`);
108
+ console.log(` Next: ${c.dim(`ks kill advance ${id} --credential-id ${opts.credentialId}`)}`);
98
109
  }
99
110
  }
100
111
  }
101
112
  catch (err) {
102
- outputError(err.message, json);
103
- process.exit(1);
113
+ handleError(err, json);
104
114
  }
105
115
  });
106
116
  kill
107
117
  .command("abort <id>")
108
118
  .description("Abort a kill sequence")
109
119
  .action(async (id) => {
110
- const json = program.opts().json;
120
+ const { json, yes } = program.opts();
111
121
  try {
112
- const data = await apiRequest(`/database/kill/${id}/abort`, { method: "POST" });
122
+ const ok = await confirm(`Abort kill sequence ${id}?`, { yes, json });
123
+ if (!ok) {
124
+ console.log("Aborted.");
125
+ return;
126
+ }
127
+ const client = createClient();
128
+ await client.database.abort(id);
113
129
  if (json) {
114
- outputJson(data);
130
+ outputJson({ aborted: true, id });
115
131
  }
116
132
  else {
117
- console.log(`Kill sequence ${id} aborted.`);
133
+ success(`Kill sequence ${id} aborted.`);
118
134
  }
119
135
  }
120
136
  catch (err) {
121
- outputError(err.message, json);
122
- process.exit(1);
137
+ handleError(err, json);
123
138
  }
124
139
  });
125
140
  }
@@ -3,22 +3,7 @@
3
3
  *
4
4
  * One-command setup for connecting cloud providers, applying protection rules,
5
5
  * and configuring alerts. Designed for both human use and AI agent automation.
6
- *
7
- * Human use (interactive prompts):
8
- * kill-switch onboard
9
- *
10
- * AI agent use (non-interactive):
11
- * kill-switch onboard \
12
- * --provider cloudflare \
13
- * --account-id YOUR_CF_ACCOUNT_ID \
14
- * --token YOUR_CF_API_TOKEN \
15
- * --name "Production" \
16
- * --shields cost-runaway,ddos \
17
- * --alert-email you@example.com
18
- *
19
- * Multi-provider (run multiple times or comma-separate):
20
- * kill-switch onboard --provider cloudflare --token ... --account-id ...
21
- * kill-switch onboard --provider aws --access-key ... --secret-key ... --region us-east-1
22
6
  */
23
7
  import { Command } from "commander";
24
- export declare function registerOnboardCommands(program: Command): void;
8
+ import type { ClientFactory } from "../types.js";
9
+ export declare function registerOnboardCommands(program: Command, createClient: ClientFactory): void;
@@ -3,26 +3,9 @@
3
3
  *
4
4
  * One-command setup for connecting cloud providers, applying protection rules,
5
5
  * and configuring alerts. Designed for both human use and AI agent automation.
6
- *
7
- * Human use (interactive prompts):
8
- * kill-switch onboard
9
- *
10
- * AI agent use (non-interactive):
11
- * kill-switch onboard \
12
- * --provider cloudflare \
13
- * --account-id YOUR_CF_ACCOUNT_ID \
14
- * --token YOUR_CF_API_TOKEN \
15
- * --name "Production" \
16
- * --shields cost-runaway,ddos \
17
- * --alert-email you@example.com
18
- *
19
- * Multi-provider (run multiple times or comma-separate):
20
- * kill-switch onboard --provider cloudflare --token ... --account-id ...
21
- * kill-switch onboard --provider aws --access-key ... --secret-key ... --region us-east-1
22
6
  */
23
- import { apiRequest } from "../api-client.js";
24
- import { outputJson, outputError } from "../output.js";
25
- import { createInterface } from "readline";
7
+ import { outputJson, outputError, handleError, spinner, success, fail } from "../output.js";
8
+ import { ask } from "../prompts.js";
26
9
  const PROVIDER_HELP = {
27
10
  cloudflare: {
28
11
  name: "Cloudflare",
@@ -77,26 +60,134 @@ const PROVIDER_HELP = {
77
60
  Run: aws configure get region
78
61
  Common values: us-east-1, us-west-2, eu-west-1`,
79
62
  },
63
+ runpod: {
64
+ name: "RunPod",
65
+ fields: "--runpod-api-key",
66
+ howToGet: `How to get this value:
67
+
68
+ API Key:
69
+ 1. Go to https://www.runpod.io/console/user/settings
70
+ 2. Scroll to "API Keys" section
71
+ 3. Click "Create API Key" (or copy an existing one)
72
+ 4. The key starts with a long alphanumeric string
73
+
74
+ Required permissions:
75
+ - Read access to pods, serverless endpoints, and network volumes
76
+ - Write access if you want auto-kill actions (stop/terminate pods, scale endpoints)`,
77
+ },
78
+ redis: {
79
+ name: "Redis",
80
+ fields: "--redis-url (self-hosted) or --redis-cloud-key + --redis-cloud-secret + --subscription-id",
81
+ howToGet: `Redis supports three deployment types:
82
+
83
+ Self-hosted Redis:
84
+ Provide a connection URL: --redis-url redis://user:pass@host:6379
85
+
86
+ Redis Cloud:
87
+ 1. Go to https://app.redislabs.com/#/account/api-keys
88
+ 2. Create an API key pair (Account Key + Secret Key)
89
+ 3. Find your subscription ID in the console
90
+ Use: --redis-cloud-key KEY --redis-cloud-secret SECRET --subscription-id ID
91
+
92
+ AWS ElastiCache:
93
+ Use AWS credentials + cluster ID:
94
+ --access-key AKIA... --secret-key ... --region us-east-1 --cluster-id my-cluster`,
95
+ },
96
+ mongodb: {
97
+ name: "MongoDB",
98
+ fields: "--mongodb-uri (self-hosted) or --atlas-public-key + --atlas-private-key + --atlas-project-id",
99
+ howToGet: `MongoDB supports two deployment types:
100
+
101
+ MongoDB Atlas:
102
+ 1. Go to Organization > Access Manager > API Keys
103
+ 2. Create a key with "Project Read Only" + "Project Cluster Manager" roles
104
+ Use: --atlas-public-key PUB --atlas-private-key PRIV --atlas-project-id PROJ --atlas-cluster-name Cluster0
105
+
106
+ Self-hosted MongoDB:
107
+ Provide a URI: --mongodb-uri mongodb+srv://user:pass@host/db`,
108
+ },
109
+ openai: {
110
+ name: "OpenAI",
111
+ fields: "--openai-api-key",
112
+ howToGet: ` 1. Go to https://platform.openai.com/api-keys
113
+ 2. Create a new API key
114
+ 3. Copy the key (starts with sk-)
115
+ Optional: --openai-org-id (from Organization Settings)`,
116
+ },
117
+ anthropic: {
118
+ name: "Anthropic",
119
+ fields: "--anthropic-api-key",
120
+ howToGet: ` 1. Go to https://console.anthropic.com/settings/keys
121
+ 2. Create a new API key
122
+ 3. Copy the key (starts with sk-ant-)
123
+ Optional: --anthropic-workspace-id`,
124
+ },
125
+ xai: {
126
+ name: "xAI (Grok)",
127
+ fields: "--xai-api-key",
128
+ howToGet: ` 1. Go to https://console.x.ai/api-keys
129
+ 2. Create a new API key
130
+ 3. Copy the key`,
131
+ },
132
+ replicate: {
133
+ name: "Replicate",
134
+ fields: "--replicate-api-token",
135
+ howToGet: ` 1. Go to https://replicate.com/account/api-tokens
136
+ 2. Create a new token
137
+ 3. Copy the token (starts with r8_)`,
138
+ },
139
+ snowflake: {
140
+ name: "Snowflake",
141
+ fields: "--snowflake-account + --snowflake-username + --snowflake-password",
142
+ howToGet: ` Account: Found in your Snowflake URL (https://<account>.snowflakecomputing.com)
143
+ Username/Password: Your Snowflake login credentials
144
+ Optional: --warehouse COMPUTE_WH --role ACCOUNTADMIN`,
145
+ },
146
+ vercel: {
147
+ name: "Vercel",
148
+ fields: "--vercel-api-token",
149
+ howToGet: ` 1. Go to https://vercel.com/account/tokens
150
+ 2. Create a new token with appropriate scope
151
+ 3. Copy the token
152
+ Optional: --vercel-team-id (from Team Settings)`,
153
+ },
154
+ datadog: {
155
+ name: "Datadog",
156
+ fields: "--datadog-api-key + --datadog-application-key",
157
+ howToGet: ` API Key: Organization Settings > API Keys
158
+ Application Key: Organization Settings > Application Keys
159
+ Optional: --datadog-site us|eu (default: us)`,
160
+ },
161
+ neo4j: {
162
+ name: "Neo4j Aura",
163
+ fields: "--neo4j-client-id + --neo4j-client-secret",
164
+ howToGet: `How to get these values:
165
+
166
+ API Credentials (OAuth2 client credentials):
167
+ 1. Go to https://console.neo4j.io/
168
+ 2. Click your account menu (top-right) > API Credentials
169
+ 3. Click "Create API Credentials"
170
+ 4. Choose the "Tenant Admin" role for full monitoring + kill switch actions
171
+ 5. Copy both the Client ID and Client Secret
172
+
173
+ Instance ID (optional):
174
+ Found in the console URL or the instance card:
175
+ https://console.neo4j.io/d/<INSTANCE_ID>/overview
176
+ Or omit to monitor all instances in your tenant.`,
177
+ },
80
178
  };
81
179
  const AVAILABLE_SHIELDS = [
82
180
  "cost-runaway", "ddos", "brute-force", "error-storm",
83
181
  "exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
84
182
  ];
85
- function ask(question) {
86
- const rl = createInterface({ input: process.stdin, output: process.stdout });
87
- return new Promise((resolve) => {
88
- rl.question(question, (answer) => {
89
- rl.close();
90
- resolve(answer.trim());
91
- });
92
- });
93
- }
94
- export function registerOnboardCommands(program) {
183
+ const ONBOARDABLE_PROVIDERS = ["cloudflare", "gcp", "aws", "runpod", "neo4j", "mongodb"];
184
+ const ONBOARDABLE_LIST = ONBOARDABLE_PROVIDERS.join(", ");
185
+ export function registerOnboardCommands(program, createClient) {
95
186
  program
96
187
  .command("onboard")
97
188
  .alias("setup")
98
189
  .description("Quick setup: connect a cloud provider, apply protection, configure alerts")
99
- .option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws")
190
+ .option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws, runpod, neo4j, mongodb")
100
191
  .option("--name <name>", "Account name (e.g., Production)")
101
192
  .option("--token <token>", "API token (Cloudflare)")
102
193
  .option("--account-id <id>", "Account ID (Cloudflare)")
@@ -105,7 +196,19 @@ export function registerOnboardCommands(program) {
105
196
  .option("--access-key <key>", "Access Key ID (AWS)")
106
197
  .option("--secret-key <key>", "Secret Access Key (AWS)")
107
198
  .option("--region <region>", "Region (AWS, default: us-east-1)")
199
+ .option("--runpod-api-key <key>", "API Key (RunPod)")
200
+ .option("--neo4j-client-id <id>", "Client ID (Neo4j Aura)")
201
+ .option("--neo4j-client-secret <secret>", "Client Secret (Neo4j Aura)")
202
+ .option("--neo4j-instance-id <id>", "Instance ID (Neo4j Aura, optional)")
203
+ .option("--mongodb-subtype <type>", "MongoDB sub-type: atlas | self-hosted (inferred if omitted)")
204
+ .option("--atlas-public-key <key>", "Public key (MongoDB Atlas API)")
205
+ .option("--atlas-private-key <key>", "Private key (MongoDB Atlas API)")
206
+ .option("--atlas-project-id <id>", "Project ID (MongoDB Atlas)")
207
+ .option("--atlas-cluster-name <name>", "Cluster name (MongoDB Atlas, optional)")
208
+ .option("--mongodb-uri <uri>", "Connection URI (self-hosted MongoDB)")
209
+ .option("--mongodb-database <name>", "Database name (self-hosted MongoDB, optional)")
108
210
  .option("--shields <presets>", "Comma-separated shield presets to apply (default: cost-runaway)")
211
+ .option("--alert-pagerduty <key>", "PagerDuty Events API v2 routing key (recommended)")
109
212
  .option("--alert-email <email>", "Email address for alerts")
110
213
  .option("--alert-discord <url>", "Discord webhook URL for alerts")
111
214
  .option("--alert-slack <url>", "Slack webhook URL for alerts")
@@ -118,14 +221,14 @@ Examples:
118
221
  # Interactive onboarding
119
222
  kill-switch onboard
120
223
 
121
- # AI agent / non-interactive: connect Cloudflare
224
+ # AI agent / non-interactive: connect Cloudflare with PagerDuty
122
225
  kill-switch onboard \\
123
226
  --provider cloudflare \\
124
227
  --account-id 14a6fa23390363382f378b5bd4a0f849 \\
125
228
  --token cf-api-token-here \\
126
229
  --name "Production" \\
127
230
  --shields cost-runaway,ddos \\
128
- --alert-email you@example.com
231
+ --alert-pagerduty YOUR_ROUTING_KEY
129
232
 
130
233
  # Show how to get Cloudflare credentials
131
234
  kill-switch onboard --help-provider cloudflare
@@ -146,7 +249,7 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
146
249
  if (opts.helpProvider) {
147
250
  const help = PROVIDER_HELP[opts.helpProvider];
148
251
  if (!help) {
149
- outputError(`Unknown provider: ${opts.helpProvider}. Use: cloudflare, gcp, aws`, json);
252
+ outputError(`Unknown provider: ${opts.helpProvider}. Use: ${ONBOARDABLE_LIST}`, json);
150
253
  process.exit(1);
151
254
  }
152
255
  if (json) {
@@ -160,12 +263,13 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
160
263
  return;
161
264
  }
162
265
  try {
266
+ const client = createClient();
163
267
  let provider = opts.provider;
164
268
  let name = opts.name;
165
269
  // Interactive mode if no provider specified
166
270
  if (!provider) {
167
271
  if (json) {
168
- outputError("--provider is required in JSON mode. Use: cloudflare, gcp, aws", json);
272
+ outputError(`--provider is required in JSON mode. Use: ${ONBOARDABLE_LIST}`, json);
169
273
  process.exit(1);
170
274
  }
171
275
  console.log("\n\u26a1 Kill Switch Onboarding\n");
@@ -174,12 +278,15 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
174
278
  console.log(" 1. cloudflare — Workers, R2, D1, Queues, Stream");
175
279
  console.log(" 2. gcp — Cloud Run, Compute, GKE, BigQuery");
176
280
  console.log(" 3. aws — EC2, Lambda, RDS, ECS, S3");
281
+ console.log(" 4. runpod — GPU Pods, Serverless Endpoints, Network Volumes");
282
+ console.log(" 5. neo4j — Neo4j Aura graph databases");
283
+ console.log(" 6. mongodb — MongoDB Atlas, self-hosted MongoDB");
177
284
  console.log();
178
- const choice = await ask("Choose a provider (1/2/3 or name): ");
179
- provider = { "1": "cloudflare", "2": "gcp", "3": "aws" }[choice] || choice;
285
+ const choice = await ask("Choose a provider (1-6 or name): ");
286
+ provider = { "1": "cloudflare", "2": "gcp", "3": "aws", "4": "runpod", "5": "neo4j", "6": "mongodb" }[choice] || choice;
180
287
  }
181
288
  if (!PROVIDER_HELP[provider]) {
182
- outputError(`Unknown provider: ${provider}. Use: cloudflare, gcp, aws`, json);
289
+ outputError(`Unknown provider: ${provider}. Use: ${ONBOARDABLE_LIST}`, json);
183
290
  process.exit(1);
184
291
  }
185
292
  if (!name && !json) {
@@ -248,15 +355,115 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
248
355
  credential.awsSecretAccessKey = secretKey;
249
356
  credential.awsRegion = region || "us-east-1";
250
357
  }
358
+ else if (provider === "runpod") {
359
+ let apiKey = opts.runpodApiKey;
360
+ if (!apiKey && !json) {
361
+ console.log("\n Tip: Create an API Key at https://www.runpod.io/console/user/settings");
362
+ apiKey = await ask(" RunPod API Key: ");
363
+ }
364
+ if (!apiKey) {
365
+ outputError(`RunPod requires ${PROVIDER_HELP.runpod.fields}`, json);
366
+ process.exit(1);
367
+ }
368
+ credential.runpodApiKey = apiKey;
369
+ }
370
+ else if (provider === "neo4j") {
371
+ let neo4jClientId = opts.neo4jClientId || process.env.NEO4J_CLIENT_ID;
372
+ let neo4jClientSecret = opts.neo4jClientSecret || process.env.NEO4J_CLIENT_SECRET;
373
+ let neo4jInstanceId = opts.neo4jInstanceId || process.env.NEO4J_INSTANCE_ID;
374
+ if (!neo4jClientId && !json) {
375
+ console.log("\n Tip: Create API Credentials at https://console.neo4j.io/ > Account > API Credentials");
376
+ neo4jClientId = await ask(" Neo4j Client ID: ");
377
+ }
378
+ if (!neo4jClientSecret && !json) {
379
+ neo4jClientSecret = await ask(" Neo4j Client Secret: ");
380
+ }
381
+ if (!neo4jInstanceId && !json) {
382
+ neo4jInstanceId = await ask(" Instance ID (Enter to monitor all): ");
383
+ }
384
+ if (!neo4jClientId || !neo4jClientSecret) {
385
+ outputError(`Neo4j requires ${PROVIDER_HELP.neo4j.fields}`, json);
386
+ process.exit(1);
387
+ }
388
+ credential.neo4jClientId = neo4jClientId;
389
+ credential.neo4jClientSecret = neo4jClientSecret;
390
+ if (neo4jInstanceId)
391
+ credential.neo4jInstanceId = neo4jInstanceId;
392
+ }
393
+ else if (provider === "mongodb") {
394
+ const hasAtlasFlags = !!(opts.atlasPublicKey || opts.atlasPrivateKey || opts.atlasProjectId);
395
+ const hasSelfHostedFlag = !!opts.mongodbUri;
396
+ if (hasAtlasFlags && hasSelfHostedFlag) {
397
+ outputError("MongoDB: pass either Atlas keys (--atlas-public-key/--atlas-private-key/--atlas-project-id) or --mongodb-uri, not both.", json);
398
+ process.exit(1);
399
+ }
400
+ let subType = opts.mongodbSubtype === "atlas" || opts.mongodbSubtype === "self-hosted"
401
+ ? opts.mongodbSubtype
402
+ : hasAtlasFlags ? "atlas" : hasSelfHostedFlag ? "self-hosted" : undefined;
403
+ if (!subType && !json) {
404
+ const answer = (await ask("MongoDB type — [a]tlas or [s]elf-hosted? ")).toLowerCase();
405
+ subType = answer.startsWith("s") ? "self-hosted" : "atlas";
406
+ }
407
+ if (!subType) {
408
+ outputError("MongoDB requires --mongodb-subtype atlas|self-hosted (or provide credentials inferring the type)", json);
409
+ process.exit(1);
410
+ }
411
+ credential.mongodbSubType = subType;
412
+ if (subType === "atlas") {
413
+ let publicKey = opts.atlasPublicKey;
414
+ let privateKey = opts.atlasPrivateKey;
415
+ let projectId = opts.atlasProjectId;
416
+ let clusterName = opts.atlasClusterName;
417
+ if (!publicKey && !json) {
418
+ console.log("\n Tip: Create at Atlas > Access Manager > API Keys (Project-level)");
419
+ console.log(" Roles: Project Read Only + Project Cluster Manager");
420
+ publicKey = await ask(" Atlas Public Key: ");
421
+ }
422
+ if (!privateKey && !json) {
423
+ privateKey = await ask(" Atlas Private Key: ");
424
+ }
425
+ if (!projectId && !json) {
426
+ console.log("\n Tip: Project ID is in the URL: cloud.mongodb.com/v2/<PROJECT_ID>");
427
+ projectId = await ask(" Atlas Project ID: ");
428
+ }
429
+ if (!clusterName && !json) {
430
+ clusterName = await ask(" Cluster name (Enter to monitor first cluster in project): ");
431
+ }
432
+ if (!publicKey || !privateKey || !projectId) {
433
+ outputError(`MongoDB Atlas requires ${PROVIDER_HELP.mongodb.fields}`, json);
434
+ process.exit(1);
435
+ }
436
+ credential.atlasPublicKey = publicKey;
437
+ credential.atlasPrivateKey = privateKey;
438
+ credential.atlasProjectId = projectId;
439
+ if (clusterName)
440
+ credential.atlasClusterName = clusterName;
441
+ }
442
+ else {
443
+ let uri = opts.mongodbUri;
444
+ if (!uri && !json) {
445
+ console.log("\n Tip: Format mongodb+srv://user:pass@host/db or mongodb://...");
446
+ uri = await ask(" MongoDB Connection URI: ");
447
+ }
448
+ if (!uri) {
449
+ outputError("MongoDB self-hosted requires --mongodb-uri", json);
450
+ process.exit(1);
451
+ }
452
+ credential.mongodbUri = uri;
453
+ if (opts.mongodbDatabase)
454
+ credential.mongodbDatabaseName = opts.mongodbDatabase;
455
+ }
456
+ }
251
457
  // 1. Connect cloud account
252
- if (!json)
253
- console.log(`\nConnecting ${PROVIDER_HELP[provider].name}...`);
254
- const account = await apiRequest("/cloud-accounts", {
255
- method: "POST",
256
- body: { provider, name, credential },
458
+ const s = json ? null : spinner(`Connecting ${PROVIDER_HELP[provider].name}...`).start();
459
+ const account = await client.accounts.create({
460
+ provider: provider,
461
+ name,
462
+ credential: credential,
257
463
  });
464
+ s?.stop();
258
465
  if (!json)
259
- console.log(`\u2713 Connected: ${account.name || account._id}`);
466
+ success(`Connected: ${account.name || account.id}`);
260
467
  // 2. Apply shields
261
468
  if (!opts.skipShields) {
262
469
  const shieldList = opts.shields
@@ -266,19 +473,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
266
473
  console.log(`\nApplying ${shieldList.length} shield(s)...`);
267
474
  for (const shield of shieldList) {
268
475
  try {
269
- await apiRequest(`/rules/presets/${shield}`, { method: "POST", body: {} });
476
+ await client.rules.applyPreset(shield);
270
477
  if (!json)
271
- console.log(` \u2713 ${shield}`);
478
+ success(` ${shield}`);
272
479
  }
273
480
  catch (err) {
274
481
  if (!json)
275
- console.log(` \u2717 ${shield}: ${err.message}`);
482
+ fail(` ${shield}: ${err.message}`);
276
483
  }
277
484
  }
278
485
  }
279
486
  // 3. Set up alerts
280
487
  if (!opts.skipAlerts) {
281
488
  const channels = [];
489
+ if (opts.alertPagerduty) {
490
+ channels.push({ type: "pagerduty", name: "PagerDuty", config: { routingKey: opts.alertPagerduty }, enabled: true });
491
+ }
282
492
  if (opts.alertEmail) {
283
493
  channels.push({ type: "email", name: "Email", config: { email: opts.alertEmail }, enabled: true });
284
494
  }
@@ -289,28 +499,30 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
289
499
  channels.push({ type: "slack", name: "Slack", config: { webhookUrl: opts.alertSlack }, enabled: true });
290
500
  }
291
501
  if (channels.length === 0 && !json) {
292
- const email = await ask("\nAlert email (or Enter to skip): ");
293
- if (email) {
294
- channels.push({ type: "email", name: "Email", config: { email }, enabled: true });
502
+ const pdKey = await ask("\nPagerDuty routing key (or Enter to skip): ");
503
+ if (pdKey) {
504
+ channels.push({ type: "pagerduty", name: "PagerDuty", config: { routingKey: pdKey }, enabled: true });
295
505
  }
296
506
  }
297
507
  if (channels.length > 0) {
298
508
  if (!json)
299
509
  console.log("Setting up alerts...");
300
510
  try {
301
- await apiRequest("/alerts/channels", { method: "PUT", body: { channels } });
511
+ // Append to existing channels re-running onboard must not wipe prior integrations.
512
+ const existing = await client.alerts.channels().catch(() => []);
513
+ await client.alerts.updateChannels([...existing, ...channels]);
302
514
  if (!json)
303
- console.log(` \u2713 ${channels.length} alert channel(s) configured`);
515
+ success(` ${channels.length} alert channel(s) configured`);
304
516
  }
305
517
  catch (err) {
306
518
  if (!json)
307
- console.log(` \u2717 Alerts: ${err.message}`);
519
+ fail(` Alerts: ${err.message}`);
308
520
  }
309
521
  }
310
522
  }
311
523
  // 4. Complete onboarding
312
524
  try {
313
- await apiRequest("/accounts/me", { method: "PATCH", body: { onboardingCompleted: true } });
525
+ await client.account.update({ onboardingCompleted: true });
314
526
  }
315
527
  catch {
316
528
  // Non-critical
@@ -319,22 +531,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
319
531
  outputJson({
320
532
  success: true,
321
533
  provider,
322
- accountId: account._id || account.id,
534
+ accountId: account.id,
323
535
  accountName: account.name,
324
536
  });
325
537
  }
326
538
  else {
327
- console.log(`\n\u2705 Setup complete! Kill Switch is monitoring your ${PROVIDER_HELP[provider].name} account.`);
539
+ console.log(`\nSetup complete! Kill Switch is monitoring your ${PROVIDER_HELP[provider].name} account.`);
328
540
  console.log("\nNext steps:");
329
- console.log(" kill-switch accounts list view connected accounts");
330
- console.log(" kill-switch check run a monitoring check");
331
- console.log(" kill-switch shield --list see all available shields");
332
- console.log(" kill-switch onboard --provider add another provider\n");
541
+ console.log(" kill-switch check run a monitoring check");
542
+ console.log(" kill-switch alerts add --type pagerduty --routing-key KEY set up on-call alerts");
543
+ console.log(" kill-switch alerts test verify alerts work");
544
+ console.log(" kill-switch accounts list view connected accounts");
545
+ console.log(" kill-switch onboard --provider <p> — add another provider\n");
333
546
  }
334
547
  }
335
548
  catch (err) {
336
- outputError(err.message, json);
337
- process.exit(1);
549
+ handleError(err, json);
338
550
  }
339
551
  });
340
552
  }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ClientFactory } from "../types.js";
3
+ export declare function registerOrgCommands(program: Command, createClient: ClientFactory): void;