@kill-switch/cli 0.1.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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * API Client — fetch wrapper with auth and error handling
3
+ */
4
+ export declare class ApiError extends Error {
5
+ status: number;
6
+ body: any;
7
+ constructor(status: number, body: any, message: string);
8
+ }
9
+ export declare function apiRequest<T = any>(path: string, opts?: {
10
+ method?: string;
11
+ body?: any;
12
+ apiKey?: string;
13
+ apiUrl?: string;
14
+ public?: boolean;
15
+ }): Promise<T>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * API Client — fetch wrapper with auth and error handling
3
+ */
4
+ import { resolveApiKey, resolveApiUrl } from "./config.js";
5
+ export class ApiError extends Error {
6
+ status;
7
+ body;
8
+ constructor(status, body, message) {
9
+ super(message);
10
+ this.status = status;
11
+ this.body = body;
12
+ this.name = "ApiError";
13
+ }
14
+ }
15
+ export async function apiRequest(path, opts = {}) {
16
+ const apiKey = resolveApiKey(opts.apiKey);
17
+ const apiUrl = resolveApiUrl(opts.apiUrl);
18
+ if (!apiKey && !opts.public) {
19
+ throw new ApiError(0, null, "Not authenticated. Run: kill-switch auth login --api-key YOUR_KEY");
20
+ }
21
+ const url = `${apiUrl}${path}`;
22
+ const headers = {
23
+ "Content-Type": "application/json",
24
+ };
25
+ if (apiKey)
26
+ headers["Authorization"] = `Bearer ${apiKey}`;
27
+ const res = await fetch(url, {
28
+ method: opts.method || "GET",
29
+ headers,
30
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
31
+ });
32
+ const text = await res.text();
33
+ let data;
34
+ try {
35
+ data = JSON.parse(text);
36
+ }
37
+ catch {
38
+ if (!res.ok)
39
+ throw new ApiError(res.status, null, `API error ${res.status}: ${text.slice(0, 200)}`);
40
+ return text;
41
+ }
42
+ if (!res.ok) {
43
+ throw new ApiError(res.status, data, data.error || `API error: ${res.status}`);
44
+ }
45
+ return data;
46
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAccountCommands(program: Command): void;
@@ -0,0 +1,133 @@
1
+ import { apiRequest } from "../api-client.js";
2
+ import { outputJson, formatTable, formatObject, outputError } from "../output.js";
3
+ export function registerAccountCommands(program) {
4
+ const accounts = program.command("accounts").description("Manage cloud accounts");
5
+ accounts
6
+ .command("list")
7
+ .alias("ls")
8
+ .description("List connected cloud accounts")
9
+ .action(async () => {
10
+ const json = program.opts().json;
11
+ try {
12
+ const data = await apiRequest("/cloud-accounts");
13
+ const list = data.accounts || data;
14
+ if (json) {
15
+ outputJson(list);
16
+ }
17
+ else {
18
+ formatTable(Array.isArray(list) ? list : [], [
19
+ { key: "_id", header: "ID" },
20
+ { key: "provider", header: "Provider" },
21
+ { key: "name", header: "Name" },
22
+ { key: "status", header: "Status" },
23
+ ]);
24
+ }
25
+ }
26
+ catch (err) {
27
+ outputError(err.message, json);
28
+ process.exit(1);
29
+ }
30
+ });
31
+ accounts
32
+ .command("get <id>")
33
+ .description("Get cloud account details")
34
+ .action(async (id) => {
35
+ const json = program.opts().json;
36
+ try {
37
+ const data = await apiRequest(`/cloud-accounts/${id}`);
38
+ if (json) {
39
+ outputJson(data);
40
+ }
41
+ else {
42
+ formatObject(data);
43
+ }
44
+ }
45
+ catch (err) {
46
+ outputError(err.message, json);
47
+ process.exit(1);
48
+ }
49
+ });
50
+ accounts
51
+ .command("add <provider>")
52
+ .description("Connect a cloud provider (cloudflare, gcp, aws)")
53
+ .requiredOption("--name <name>", "Account name")
54
+ .option("--token <token>", "API token (Cloudflare)")
55
+ .option("--account-id <id>", "Account ID (Cloudflare)")
56
+ .option("--project-id <id>", "Project ID (GCP)")
57
+ .option("--service-account <json>", "Service Account JSON (GCP)")
58
+ .action(async (provider, opts) => {
59
+ const json = program.opts().json;
60
+ const credential = {};
61
+ if (opts.token)
62
+ credential.apiToken = opts.token;
63
+ if (opts.accountId)
64
+ credential.accountId = opts.accountId;
65
+ if (opts.projectId)
66
+ credential.projectId = opts.projectId;
67
+ if (opts.serviceAccount)
68
+ credential.serviceAccountJson = opts.serviceAccount;
69
+ try {
70
+ const data = await apiRequest("/cloud-accounts", {
71
+ method: "POST",
72
+ body: { provider, name: opts.name, credential },
73
+ });
74
+ if (json) {
75
+ outputJson(data);
76
+ }
77
+ else {
78
+ console.log(`Connected ${provider} account: ${data.name || data._id}`);
79
+ }
80
+ }
81
+ catch (err) {
82
+ outputError(err.message, json);
83
+ process.exit(1);
84
+ }
85
+ });
86
+ accounts
87
+ .command("delete <id>")
88
+ .alias("rm")
89
+ .description("Disconnect and delete a cloud account")
90
+ .action(async (id) => {
91
+ const json = program.opts().json;
92
+ try {
93
+ await apiRequest(`/cloud-accounts/${id}`, { method: "DELETE" });
94
+ if (json) {
95
+ outputJson({ deleted: true, id });
96
+ }
97
+ else {
98
+ console.log(`Account ${id} disconnected.`);
99
+ }
100
+ }
101
+ catch (err) {
102
+ outputError(err.message, json);
103
+ process.exit(1);
104
+ }
105
+ });
106
+ accounts
107
+ .command("check <id>")
108
+ .description("Run manual monitoring check on an account")
109
+ .action(async (id) => {
110
+ const json = program.opts().json;
111
+ try {
112
+ const data = await apiRequest(`/cloud-accounts/${id}/check`, { method: "POST" });
113
+ if (json) {
114
+ outputJson(data);
115
+ }
116
+ else {
117
+ console.log(`Check complete: ${data.violations?.length || 0} violations`);
118
+ if (data.violations?.length) {
119
+ formatTable(data.violations, [
120
+ { key: "metric", header: "Metric" },
121
+ { key: "value", header: "Value" },
122
+ { key: "threshold", header: "Threshold" },
123
+ { key: "action", header: "Action" },
124
+ ]);
125
+ }
126
+ }
127
+ }
128
+ catch (err) {
129
+ outputError(err.message, json);
130
+ process.exit(1);
131
+ }
132
+ });
133
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAlertCommands(program: Command): void;
@@ -0,0 +1,49 @@
1
+ import { apiRequest } from "../api-client.js";
2
+ import { outputJson, formatTable, outputError } from "../output.js";
3
+ export function registerAlertCommands(program) {
4
+ const alerts = program.command("alerts").description("Manage alert channels");
5
+ alerts
6
+ .command("list")
7
+ .alias("ls")
8
+ .description("List configured alert channels")
9
+ .action(async () => {
10
+ const json = program.opts().json;
11
+ try {
12
+ const data = await apiRequest("/alerts/channels");
13
+ const channels = data.channels || data;
14
+ if (json) {
15
+ outputJson(channels);
16
+ }
17
+ else {
18
+ formatTable(Array.isArray(channels) ? channels : [], [
19
+ { key: "type", header: "Type" },
20
+ { key: "name", header: "Name" },
21
+ { key: "enabled", header: "Enabled" },
22
+ ]);
23
+ }
24
+ }
25
+ catch (err) {
26
+ outputError(err.message, json);
27
+ process.exit(1);
28
+ }
29
+ });
30
+ alerts
31
+ .command("test")
32
+ .description("Send a test alert to all channels")
33
+ .action(async () => {
34
+ const json = program.opts().json;
35
+ try {
36
+ const data = await apiRequest("/alerts/test", { method: "POST" });
37
+ if (json) {
38
+ outputJson(data);
39
+ }
40
+ else {
41
+ console.log("Test alert sent to all configured channels.");
42
+ }
43
+ }
44
+ catch (err) {
45
+ outputError(err.message, json);
46
+ process.exit(1);
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAnalyticsCommands(program: Command): void;
@@ -0,0 +1,35 @@
1
+ import { apiRequest } from "../api-client.js";
2
+ import { outputJson, formatTable, outputError } from "../output.js";
3
+ export function registerAnalyticsCommands(program) {
4
+ program
5
+ .command("analytics")
6
+ .description("FinOps analytics overview")
7
+ .option("--days <n>", "Days to analyze", "30")
8
+ .action(async (opts) => {
9
+ const json = program.opts().json;
10
+ try {
11
+ const data = await apiRequest(`/analytics/overview?days=${opts.days}`);
12
+ if (json) {
13
+ outputJson(data);
14
+ }
15
+ else {
16
+ console.log(`Analytics (last ${opts.days} days)\n`);
17
+ if (data.dailyCosts) {
18
+ 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" },
23
+ ]);
24
+ }
25
+ if (data.totalSavingsUsd !== undefined) {
26
+ console.log(`\nEstimated savings: $${data.totalSavingsUsd}`);
27
+ }
28
+ }
29
+ }
30
+ catch (err) {
31
+ outputError(err.message, json);
32
+ process.exit(1);
33
+ }
34
+ });
35
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAuthCommands(program: Command): void;
@@ -0,0 +1,85 @@
1
+ import { saveConfig, deleteConfig, resolveApiKey, resolveApiUrl } from "../config.js";
2
+ import { apiRequest } from "../api-client.js";
3
+ import { outputJson, formatObject, outputError } from "../output.js";
4
+ export function registerAuthCommands(program) {
5
+ const auth = program.command("auth").description("Manage authentication");
6
+ auth
7
+ .command("login")
8
+ .description("Authenticate with an API key")
9
+ .requiredOption("--api-key <key>", "Personal API key (starts with ks_)")
10
+ .action(async (opts) => {
11
+ const json = program.opts().json;
12
+ const key = opts.apiKey;
13
+ if (!key.startsWith("ks_")) {
14
+ outputError("API key must start with 'ks_'. Create one at app.kill-switch.net.", json);
15
+ process.exit(1);
16
+ }
17
+ // Validate the key by calling the API
18
+ try {
19
+ const result = await apiRequest("/accounts/me", { apiKey: key });
20
+ saveConfig({ apiKey: key, apiUrl: resolveApiUrl() });
21
+ if (json) {
22
+ outputJson({ authenticated: true, account: result.name || result._id });
23
+ }
24
+ else {
25
+ console.log(`Authenticated as ${result.name || result._id}`);
26
+ console.log("API key saved to ~/.kill-switch/config.json");
27
+ }
28
+ }
29
+ catch (err) {
30
+ outputError(`Authentication failed: ${err.message}`, json);
31
+ process.exit(2);
32
+ }
33
+ });
34
+ auth
35
+ .command("logout")
36
+ .description("Clear stored credentials")
37
+ .action(() => {
38
+ const json = program.opts().json;
39
+ deleteConfig();
40
+ if (json) {
41
+ outputJson({ loggedOut: true });
42
+ }
43
+ else {
44
+ console.log("Credentials cleared.");
45
+ }
46
+ });
47
+ auth
48
+ .command("status")
49
+ .description("Show current auth status")
50
+ .action(async () => {
51
+ const json = program.opts().json;
52
+ const key = resolveApiKey();
53
+ if (!key) {
54
+ if (json) {
55
+ outputJson({ authenticated: false });
56
+ }
57
+ else {
58
+ console.log("Not authenticated. Run: kill-switch auth login --api-key YOUR_KEY");
59
+ }
60
+ return;
61
+ }
62
+ try {
63
+ const result = await apiRequest("/accounts/me");
64
+ if (json) {
65
+ outputJson({ authenticated: true, ...result });
66
+ }
67
+ else {
68
+ formatObject({
69
+ authenticated: "yes",
70
+ account: result.name || result._id,
71
+ tier: result.tier,
72
+ keyPrefix: key.substring(0, 16) + "...",
73
+ });
74
+ }
75
+ }
76
+ catch {
77
+ if (json) {
78
+ outputJson({ authenticated: false, keyPresent: true, error: "Key is invalid or expired" });
79
+ }
80
+ else {
81
+ console.log("API key present but invalid. Run: kill-switch auth login --api-key NEW_KEY");
82
+ }
83
+ }
84
+ });
85
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCheckCommands(program: Command): void;
@@ -0,0 +1,37 @@
1
+ import { apiRequest } from "../api-client.js";
2
+ import { outputJson, formatTable, outputError } from "../output.js";
3
+ export function registerCheckCommands(program) {
4
+ program
5
+ .command("check")
6
+ .description("Run monitoring check on all connected accounts")
7
+ .action(async () => {
8
+ const json = program.opts().json;
9
+ try {
10
+ const data = await apiRequest("/check", { method: "POST" });
11
+ if (json) {
12
+ outputJson(data);
13
+ }
14
+ else {
15
+ const results = data.results || [];
16
+ console.log(`Checked ${results.length} account(s)\n`);
17
+ for (const r of results) {
18
+ console.log(`${r.provider || "unknown"}: ${r.name || r.cloudAccountId}`);
19
+ if (r.violations?.length) {
20
+ formatTable(r.violations, [
21
+ { key: "metric", header: "Metric" },
22
+ { key: "value", header: "Value" },
23
+ { key: "threshold", header: "Threshold" },
24
+ ]);
25
+ }
26
+ else {
27
+ console.log(" All clear\n");
28
+ }
29
+ }
30
+ }
31
+ }
32
+ catch (err) {
33
+ outputError(err.message, json);
34
+ process.exit(1);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerConfigCommands(program: Command): void;
@@ -0,0 +1,72 @@
1
+ import { loadConfig, saveConfig, CONFIG_FILE, DEFAULT_API_URL } from "../config.js";
2
+ import { outputJson } from "../output.js";
3
+ export function registerConfigCommands(program) {
4
+ const config = program.command("config").description("Manage CLI configuration");
5
+ config
6
+ .command("init")
7
+ .description("Create config file with defaults")
8
+ .action(() => {
9
+ const json = program.opts().json;
10
+ const existing = loadConfig();
11
+ saveConfig({ apiUrl: DEFAULT_API_URL, ...existing });
12
+ if (json) {
13
+ outputJson({ configFile: CONFIG_FILE, created: true });
14
+ }
15
+ else {
16
+ console.log(`Config saved to ${CONFIG_FILE}`);
17
+ }
18
+ });
19
+ config
20
+ .command("get <key>")
21
+ .description("Get a config value")
22
+ .action((key) => {
23
+ const json = program.opts().json;
24
+ const cfg = loadConfig();
25
+ const value = cfg[key];
26
+ if (json) {
27
+ outputJson({ [key]: value ?? null });
28
+ }
29
+ else {
30
+ console.log(value ?? "(not set)");
31
+ }
32
+ });
33
+ config
34
+ .command("set <key> <value>")
35
+ .description("Set a config value")
36
+ .action((key, value) => {
37
+ const json = program.opts().json;
38
+ const cfg = loadConfig();
39
+ cfg[key] = value;
40
+ saveConfig(cfg);
41
+ if (json) {
42
+ outputJson({ [key]: value });
43
+ }
44
+ else {
45
+ console.log(`${key} = ${value}`);
46
+ }
47
+ });
48
+ config
49
+ .command("list")
50
+ .alias("ls")
51
+ .description("Show all config values")
52
+ .action(() => {
53
+ const json = program.opts().json;
54
+ const cfg = loadConfig();
55
+ if (json) {
56
+ outputJson(cfg);
57
+ }
58
+ else {
59
+ const entries = Object.entries(cfg);
60
+ if (entries.length === 0) {
61
+ console.log("No config set. Run: kill-switch config init");
62
+ }
63
+ else {
64
+ for (const [k, v] of entries) {
65
+ const display = k === "apiKey" ? String(v).substring(0, 16) + "..." : String(v);
66
+ console.log(`${k.padEnd(12)} ${display}`);
67
+ }
68
+ }
69
+ console.log(`\nConfig file: ${CONFIG_FILE}`);
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerKillCommands(program: Command): void;
@@ -0,0 +1,125 @@
1
+ import { apiRequest } from "../api-client.js";
2
+ import { outputJson, formatTable, formatObject, outputError } from "../output.js";
3
+ export function registerKillCommands(program) {
4
+ const kill = program.command("kill").description("Database kill switch sequences");
5
+ kill
6
+ .command("init")
7
+ .description("Initiate a database kill sequence")
8
+ .requiredOption("--credential-id <id>", "Stored credential ID")
9
+ .requiredOption("--trigger <reason>", "Kill trigger reason")
10
+ .action(async (opts) => {
11
+ const json = program.opts().json;
12
+ try {
13
+ const data = await apiRequest("/database/kill", {
14
+ method: "POST",
15
+ body: { credentialId: opts.credentialId, trigger: opts.trigger },
16
+ });
17
+ if (json) {
18
+ outputJson(data);
19
+ }
20
+ 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}`);
25
+ }
26
+ }
27
+ catch (err) {
28
+ outputError(err.message, json);
29
+ process.exit(1);
30
+ }
31
+ });
32
+ kill
33
+ .command("status [id]")
34
+ .description("Get kill sequence status (or list all active)")
35
+ .action(async (id) => {
36
+ const json = program.opts().json;
37
+ try {
38
+ if (id) {
39
+ const data = await apiRequest(`/database/kill/${id}`);
40
+ if (json) {
41
+ outputJson(data);
42
+ }
43
+ else {
44
+ formatObject(data, ["id", "status", "currentStep", "snapshotVerified"]);
45
+ if (data.steps) {
46
+ console.log("\nSteps:");
47
+ formatTable(data.steps, [
48
+ { key: "action", header: "Action" },
49
+ { key: "status", header: "Status" },
50
+ { key: "timestamp", header: "Timestamp" },
51
+ ]);
52
+ }
53
+ }
54
+ }
55
+ else {
56
+ const data = await apiRequest("/database/kill");
57
+ const sequences = data.sequences || data;
58
+ if (json) {
59
+ outputJson(sequences);
60
+ }
61
+ else {
62
+ formatTable(Array.isArray(sequences) ? sequences : [], [
63
+ { key: "id", header: "Sequence ID" },
64
+ { key: "status", header: "Status" },
65
+ { key: "currentStep", header: "Step" },
66
+ ]);
67
+ }
68
+ }
69
+ }
70
+ catch (err) {
71
+ outputError(err.message, json);
72
+ process.exit(1);
73
+ }
74
+ });
75
+ kill
76
+ .command("advance <id>")
77
+ .description("Execute the next step in a kill sequence")
78
+ .requiredOption("--credential-id <credId>", "Stored credential ID")
79
+ .option("--human-approval", "Confirm human approval (required for nuke step)")
80
+ .action(async (id, opts) => {
81
+ const json = program.opts().json;
82
+ 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
+ });
90
+ if (json) {
91
+ outputJson(data);
92
+ }
93
+ else {
94
+ console.log(`Step executed: ${data.steps?.[data.currentStep - 1]?.action || "?"}`);
95
+ console.log(`Status: ${data.status}`);
96
+ if (data.status !== "completed") {
97
+ console.log(`Next: kill-switch kill advance ${id} --credential-id ${opts.credentialId}`);
98
+ }
99
+ }
100
+ }
101
+ catch (err) {
102
+ outputError(err.message, json);
103
+ process.exit(1);
104
+ }
105
+ });
106
+ kill
107
+ .command("abort <id>")
108
+ .description("Abort a kill sequence")
109
+ .action(async (id) => {
110
+ const json = program.opts().json;
111
+ try {
112
+ const data = await apiRequest(`/database/kill/${id}/abort`, { method: "POST" });
113
+ if (json) {
114
+ outputJson(data);
115
+ }
116
+ else {
117
+ console.log(`Kill sequence ${id} aborted.`);
118
+ }
119
+ }
120
+ catch (err) {
121
+ outputError(err.message, json);
122
+ process.exit(1);
123
+ }
124
+ });
125
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Onboard Command
3
+ *
4
+ * One-command setup for connecting cloud providers, applying protection rules,
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
+ */
23
+ import { Command } from "commander";
24
+ export declare function registerOnboardCommands(program: Command): void;