@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.
- package/dist/api-client.d.ts +15 -0
- package/dist/api-client.js +46 -0
- package/dist/commands/accounts.d.ts +2 -0
- package/dist/commands/accounts.js +133 -0
- package/dist/commands/alerts.d.ts +2 -0
- package/dist/commands/alerts.js +49 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +35 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +85 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +37 -0
- package/dist/commands/config-cmd.d.ts +2 -0
- package/dist/commands/config-cmd.js +72 -0
- package/dist/commands/kill.d.ts +2 -0
- package/dist/commands/kill.js +125 -0
- package/dist/commands/onboard.d.ts +24 -0
- package/dist/commands/onboard.js +340 -0
- package/dist/commands/rules.d.ts +2 -0
- package/dist/commands/rules.js +121 -0
- package/dist/commands/shield.d.ts +2 -0
- package/dist/commands/shield.js +57 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +49 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +42 -0
- package/dist/output.d.ts +18 -0
- package/dist/output.js +60 -0
- package/package.json +28 -0
|
@@ -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,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,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,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,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,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,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,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;
|