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