@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,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",
@@ -92,26 +75,119 @@ const PROVIDER_HELP = {
92
75
  - Read access to pods, serverless endpoints, and network volumes
93
76
  - Write access if you want auto-kill actions (stop/terminate pods, scale endpoints)`,
94
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
+ },
95
178
  };
96
179
  const AVAILABLE_SHIELDS = [
97
180
  "cost-runaway", "ddos", "brute-force", "error-storm",
98
181
  "exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
99
182
  ];
100
- function ask(question) {
101
- const rl = createInterface({ input: process.stdin, output: process.stdout });
102
- return new Promise((resolve) => {
103
- rl.question(question, (answer) => {
104
- rl.close();
105
- resolve(answer.trim());
106
- });
107
- });
108
- }
109
- 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) {
110
186
  program
111
187
  .command("onboard")
112
188
  .alias("setup")
113
189
  .description("Quick setup: connect a cloud provider, apply protection, configure alerts")
114
- .option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws")
190
+ .option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws, runpod, neo4j, mongodb")
115
191
  .option("--name <name>", "Account name (e.g., Production)")
116
192
  .option("--token <token>", "API token (Cloudflare)")
117
193
  .option("--account-id <id>", "Account ID (Cloudflare)")
@@ -121,7 +197,18 @@ export function registerOnboardCommands(program) {
121
197
  .option("--secret-key <key>", "Secret Access Key (AWS)")
122
198
  .option("--region <region>", "Region (AWS, default: us-east-1)")
123
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)")
124
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)")
125
212
  .option("--alert-email <email>", "Email address for alerts")
126
213
  .option("--alert-discord <url>", "Discord webhook URL for alerts")
127
214
  .option("--alert-slack <url>", "Slack webhook URL for alerts")
@@ -134,14 +221,14 @@ Examples:
134
221
  # Interactive onboarding
135
222
  kill-switch onboard
136
223
 
137
- # AI agent / non-interactive: connect Cloudflare
224
+ # AI agent / non-interactive: connect Cloudflare with PagerDuty
138
225
  kill-switch onboard \\
139
226
  --provider cloudflare \\
140
227
  --account-id 14a6fa23390363382f378b5bd4a0f849 \\
141
228
  --token cf-api-token-here \\
142
229
  --name "Production" \\
143
230
  --shields cost-runaway,ddos \\
144
- --alert-email you@example.com
231
+ --alert-pagerduty YOUR_ROUTING_KEY
145
232
 
146
233
  # Show how to get Cloudflare credentials
147
234
  kill-switch onboard --help-provider cloudflare
@@ -162,7 +249,7 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
162
249
  if (opts.helpProvider) {
163
250
  const help = PROVIDER_HELP[opts.helpProvider];
164
251
  if (!help) {
165
- outputError(`Unknown provider: ${opts.helpProvider}. Use: cloudflare, gcp, aws, runpod`, json);
252
+ outputError(`Unknown provider: ${opts.helpProvider}. Use: ${ONBOARDABLE_LIST}`, json);
166
253
  process.exit(1);
167
254
  }
168
255
  if (json) {
@@ -176,12 +263,13 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
176
263
  return;
177
264
  }
178
265
  try {
266
+ const client = createClient();
179
267
  let provider = opts.provider;
180
268
  let name = opts.name;
181
269
  // Interactive mode if no provider specified
182
270
  if (!provider) {
183
271
  if (json) {
184
- outputError("--provider is required in JSON mode. Use: cloudflare, gcp, aws, runpod", json);
272
+ outputError(`--provider is required in JSON mode. Use: ${ONBOARDABLE_LIST}`, json);
185
273
  process.exit(1);
186
274
  }
187
275
  console.log("\n\u26a1 Kill Switch Onboarding\n");
@@ -191,12 +279,14 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
191
279
  console.log(" 2. gcp — Cloud Run, Compute, GKE, BigQuery");
192
280
  console.log(" 3. aws — EC2, Lambda, RDS, ECS, S3");
193
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");
194
284
  console.log();
195
- const choice = await ask("Choose a provider (1/2/3/4 or name): ");
196
- provider = { "1": "cloudflare", "2": "gcp", "3": "aws", "4": "runpod" }[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;
197
287
  }
198
288
  if (!PROVIDER_HELP[provider]) {
199
- outputError(`Unknown provider: ${provider}. Use: cloudflare, gcp, aws, runpod`, json);
289
+ outputError(`Unknown provider: ${provider}. Use: ${ONBOARDABLE_LIST}`, json);
200
290
  process.exit(1);
201
291
  }
202
292
  if (!name && !json) {
@@ -277,15 +367,103 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
277
367
  }
278
368
  credential.runpodApiKey = apiKey;
279
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
+ }
280
457
  // 1. Connect cloud account
281
- if (!json)
282
- console.log(`\nConnecting ${PROVIDER_HELP[provider].name}...`);
283
- const account = await apiRequest("/cloud-accounts", {
284
- method: "POST",
285
- 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,
286
463
  });
464
+ s?.stop();
287
465
  if (!json)
288
- console.log(`\u2713 Connected: ${account.name || account._id}`);
466
+ success(`Connected: ${account.name || account.id}`);
289
467
  // 2. Apply shields
290
468
  if (!opts.skipShields) {
291
469
  const shieldList = opts.shields
@@ -295,19 +473,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
295
473
  console.log(`\nApplying ${shieldList.length} shield(s)...`);
296
474
  for (const shield of shieldList) {
297
475
  try {
298
- await apiRequest(`/rules/presets/${shield}`, { method: "POST", body: {} });
476
+ await client.rules.applyPreset(shield);
299
477
  if (!json)
300
- console.log(` \u2713 ${shield}`);
478
+ success(` ${shield}`);
301
479
  }
302
480
  catch (err) {
303
481
  if (!json)
304
- console.log(` \u2717 ${shield}: ${err.message}`);
482
+ fail(` ${shield}: ${err.message}`);
305
483
  }
306
484
  }
307
485
  }
308
486
  // 3. Set up alerts
309
487
  if (!opts.skipAlerts) {
310
488
  const channels = [];
489
+ if (opts.alertPagerduty) {
490
+ channels.push({ type: "pagerduty", name: "PagerDuty", config: { routingKey: opts.alertPagerduty }, enabled: true });
491
+ }
311
492
  if (opts.alertEmail) {
312
493
  channels.push({ type: "email", name: "Email", config: { email: opts.alertEmail }, enabled: true });
313
494
  }
@@ -318,28 +499,30 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
318
499
  channels.push({ type: "slack", name: "Slack", config: { webhookUrl: opts.alertSlack }, enabled: true });
319
500
  }
320
501
  if (channels.length === 0 && !json) {
321
- const email = await ask("\nAlert email (or Enter to skip): ");
322
- if (email) {
323
- 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 });
324
505
  }
325
506
  }
326
507
  if (channels.length > 0) {
327
508
  if (!json)
328
509
  console.log("Setting up alerts...");
329
510
  try {
330
- 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]);
331
514
  if (!json)
332
- console.log(` \u2713 ${channels.length} alert channel(s) configured`);
515
+ success(` ${channels.length} alert channel(s) configured`);
333
516
  }
334
517
  catch (err) {
335
518
  if (!json)
336
- console.log(` \u2717 Alerts: ${err.message}`);
519
+ fail(` Alerts: ${err.message}`);
337
520
  }
338
521
  }
339
522
  }
340
523
  // 4. Complete onboarding
341
524
  try {
342
- await apiRequest("/accounts/me", { method: "PATCH", body: { onboardingCompleted: true } });
525
+ await client.account.update({ onboardingCompleted: true });
343
526
  }
344
527
  catch {
345
528
  // Non-critical
@@ -348,22 +531,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
348
531
  outputJson({
349
532
  success: true,
350
533
  provider,
351
- accountId: account._id || account.id,
534
+ accountId: account.id,
352
535
  accountName: account.name,
353
536
  });
354
537
  }
355
538
  else {
356
- 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.`);
357
540
  console.log("\nNext steps:");
358
- console.log(" kill-switch accounts list view connected accounts");
359
- console.log(" kill-switch check run a monitoring check");
360
- console.log(" kill-switch shield --list see all available shields");
361
- 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");
362
546
  }
363
547
  }
364
548
  catch (err) {
365
- outputError(err.message, json);
366
- process.exit(1);
549
+ handleError(err, json);
367
550
  }
368
551
  });
369
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;