@kill-switch/cli 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +127 -14
- package/dist/commands/check.d.ts +2 -1
- package/dist/commands/check.js +28 -15
- 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 +273 -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/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,68 @@
|
|
|
1
|
+
import { outputJson, handleError, spinner, success, warn, fail, colors as c } from "../output.js";
|
|
2
|
+
export function registerWatchCommand(program, createClient) {
|
|
3
|
+
program
|
|
4
|
+
.command("watch")
|
|
5
|
+
.description("Continuously monitor all accounts (polls on interval)")
|
|
6
|
+
.option("--interval <seconds>", "Check interval in seconds", "60")
|
|
7
|
+
.action(async (opts) => {
|
|
8
|
+
const json = program.opts().json;
|
|
9
|
+
const intervalSec = Math.max(10, parseInt(opts.interval) || 60);
|
|
10
|
+
const client = createClient();
|
|
11
|
+
if (!json) {
|
|
12
|
+
console.log(c.bold(`\nKill Switch Watch Mode`) + c.dim(` (every ${intervalSec}s, Ctrl+C to stop)\n`));
|
|
13
|
+
}
|
|
14
|
+
const runCheck = async () => {
|
|
15
|
+
const s = json ? null : spinner("Checking...").start();
|
|
16
|
+
try {
|
|
17
|
+
const data = await client.monitoring.checkAll();
|
|
18
|
+
s?.stop();
|
|
19
|
+
const results = data.results || [];
|
|
20
|
+
const now = new Date().toLocaleTimeString();
|
|
21
|
+
if (json) {
|
|
22
|
+
outputJson({ checkedAt: now, ...data });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const totalViolations = results.reduce((sum, r) => sum + (r.violations?.length || 0), 0);
|
|
26
|
+
if (totalViolations === 0) {
|
|
27
|
+
success(`${c.dim(now)} All ${results.length} account(s) clear`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
warn(`${c.dim(now)} ${c.bold(String(totalViolations))} violation(s) across ${results.length} account(s)`);
|
|
31
|
+
for (const r of results) {
|
|
32
|
+
if (r.violations?.length) {
|
|
33
|
+
console.log(` ${c.bold((r.provider || "?") + ":")} ${r.name || r.cloudAccountId}`);
|
|
34
|
+
for (const v of r.violations) {
|
|
35
|
+
console.log(` ${c.red("!")} ${v.metricName}: ${v.currentValue} ${v.unit} (threshold: ${v.threshold})`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
s?.stop();
|
|
43
|
+
if (json) {
|
|
44
|
+
handleError(err, json);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const now = new Date().toLocaleTimeString();
|
|
48
|
+
fail(`${c.dim(now)} Check failed: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Run immediately
|
|
53
|
+
await runCheck();
|
|
54
|
+
// Then loop
|
|
55
|
+
const timer = setInterval(runCheck, intervalSec * 1000);
|
|
56
|
+
// Graceful shutdown
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
clearInterval(timer);
|
|
59
|
+
if (!json)
|
|
60
|
+
console.log(c.dim("\nWatch stopped."));
|
|
61
|
+
process.exit(0);
|
|
62
|
+
};
|
|
63
|
+
process.on("SIGINT", cleanup);
|
|
64
|
+
process.on("SIGTERM", cleanup);
|
|
65
|
+
// Keep alive
|
|
66
|
+
await new Promise(() => { });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI device-flow login helper.
|
|
3
|
+
*
|
|
4
|
+
* Runs the start → openBrowser → poll → return-key sequence against the
|
|
5
|
+
* Kill Switch API. Side effects (fetch, browser opening, spinner output,
|
|
6
|
+
* sleep, hostname) are injected via `deps` so the loop logic can be unit
|
|
7
|
+
* tested without touching the network, terminal, or system clock.
|
|
8
|
+
*/
|
|
9
|
+
export interface DeviceFlowStart {
|
|
10
|
+
code: string;
|
|
11
|
+
verification_url: string;
|
|
12
|
+
expires_in: number;
|
|
13
|
+
polling_interval: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DeviceFlowDeps {
|
|
16
|
+
fetch: typeof fetch;
|
|
17
|
+
sleep: (ms: number) => Promise<void>;
|
|
18
|
+
openBrowser: (url: string) => void;
|
|
19
|
+
hostname: () => string;
|
|
20
|
+
/** Called with status lines for the user — wired to console.log in prod, no-op in tests. */
|
|
21
|
+
log: (line: string) => void;
|
|
22
|
+
/** Spinner start/stop wrappers — wire to ora in prod, no-op in tests. */
|
|
23
|
+
spinnerStart: (label: string) => void;
|
|
24
|
+
spinnerStop: () => void;
|
|
25
|
+
/** Source of "now" in ms — overridable in tests for deadline math. */
|
|
26
|
+
now: () => number;
|
|
27
|
+
}
|
|
28
|
+
export declare function defaultDeviceFlowDeps(extras?: Partial<DeviceFlowDeps>): DeviceFlowDeps;
|
|
29
|
+
/**
|
|
30
|
+
* Run the device-flow login. Returns the freshly-minted API key on success;
|
|
31
|
+
* throws on user denial, expiry, /start failure, or 10-minute timeout.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runDeviceFlow(apiUrl: string, cliVersion: string, json: boolean, deps?: DeviceFlowDeps): Promise<string>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI device-flow login helper.
|
|
3
|
+
*
|
|
4
|
+
* Runs the start → openBrowser → poll → return-key sequence against the
|
|
5
|
+
* Kill Switch API. Side effects (fetch, browser opening, spinner output,
|
|
6
|
+
* sleep, hostname) are injected via `deps` so the loop logic can be unit
|
|
7
|
+
* tested without touching the network, terminal, or system clock.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { hostname as osHostname } from "node:os";
|
|
11
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
const defaultOpenBrowser = (url) => {
|
|
13
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
14
|
+
execFile(cmd, [url], () => { });
|
|
15
|
+
};
|
|
16
|
+
export function defaultDeviceFlowDeps(extras) {
|
|
17
|
+
return {
|
|
18
|
+
fetch,
|
|
19
|
+
sleep: defaultSleep,
|
|
20
|
+
openBrowser: defaultOpenBrowser,
|
|
21
|
+
hostname: osHostname,
|
|
22
|
+
log: (line) => console.log(line),
|
|
23
|
+
spinnerStart: () => { },
|
|
24
|
+
spinnerStop: () => { },
|
|
25
|
+
now: () => Date.now(),
|
|
26
|
+
...extras,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run the device-flow login. Returns the freshly-minted API key on success;
|
|
31
|
+
* throws on user denial, expiry, /start failure, or 10-minute timeout.
|
|
32
|
+
*/
|
|
33
|
+
export async function runDeviceFlow(apiUrl, cliVersion, json, deps = defaultDeviceFlowDeps()) {
|
|
34
|
+
// 1. Start session — anonymous, server mints the short-lived code.
|
|
35
|
+
const startResp = await deps.fetch(`${apiUrl}/auth/cli/start`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ hostname: deps.hostname(), cliVersion }),
|
|
39
|
+
});
|
|
40
|
+
if (!startResp.ok) {
|
|
41
|
+
const text = await startResp.text().catch(() => "");
|
|
42
|
+
throw new Error(`Failed to start CLI auth (${startResp.status}): ${text.slice(0, 200)}`);
|
|
43
|
+
}
|
|
44
|
+
const start = (await startResp.json());
|
|
45
|
+
if (!json) {
|
|
46
|
+
deps.log("\n⚡ Kill Switch CLI Login\n");
|
|
47
|
+
deps.log("Open this URL in your browser to authorize:");
|
|
48
|
+
deps.log(` ${start.verification_url}\n`);
|
|
49
|
+
deps.log("Confirm this code matches what you see on the page:");
|
|
50
|
+
deps.log(` ${start.code}\n`);
|
|
51
|
+
}
|
|
52
|
+
deps.openBrowser(start.verification_url);
|
|
53
|
+
// 2. Poll until approved / denied / expired / timeout.
|
|
54
|
+
// `expires_in` is server-controlled (currently 600s / 10 min).
|
|
55
|
+
// `polling_interval` is also server-controlled — we floor it at 1s
|
|
56
|
+
// so a server bug can't crash us into a busy loop.
|
|
57
|
+
const deadline = deps.now() + start.expires_in * 1000;
|
|
58
|
+
const intervalMs = Math.max(1, start.polling_interval) * 1000;
|
|
59
|
+
if (!json)
|
|
60
|
+
deps.spinnerStart("Waiting for browser authorization...");
|
|
61
|
+
try {
|
|
62
|
+
while (deps.now() < deadline) {
|
|
63
|
+
await deps.sleep(intervalMs);
|
|
64
|
+
const pollResp = await deps.fetch(`${apiUrl}/auth/cli/poll`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify({ code: start.code }),
|
|
68
|
+
});
|
|
69
|
+
const body = (await pollResp.json().catch(() => ({})));
|
|
70
|
+
if (pollResp.status === 200 && body.api_key) {
|
|
71
|
+
deps.spinnerStop();
|
|
72
|
+
return body.api_key;
|
|
73
|
+
}
|
|
74
|
+
if (pollResp.status === 410) {
|
|
75
|
+
deps.spinnerStop();
|
|
76
|
+
throw new Error(`CLI login ${body.status || "ended"} — please run \`ks auth login\` again.`);
|
|
77
|
+
}
|
|
78
|
+
if (pollResp.status === 404) {
|
|
79
|
+
deps.spinnerStop();
|
|
80
|
+
throw new Error("CLI login code expired before any response was recorded.");
|
|
81
|
+
}
|
|
82
|
+
// 202 pending → loop. Any other code → also loop and let the deadline catch us.
|
|
83
|
+
}
|
|
84
|
+
deps.spinnerStop();
|
|
85
|
+
throw new Error("CLI login timed out after 10 minutes. Run `ks auth login` to try again.");
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
deps.spinnerStop();
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* kill-switch accounts list --json
|
|
12
12
|
*/
|
|
13
13
|
import { Command } from "commander";
|
|
14
|
+
import { KillSwitchClient } from "@kill-switch/sdk";
|
|
15
|
+
import { resolveApiKey, resolveApiUrl } from "./config.js";
|
|
14
16
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
15
17
|
import { registerAccountCommands } from "./commands/accounts.js";
|
|
16
18
|
import { registerRuleCommands } from "./commands/rules.js";
|
|
@@ -21,22 +23,47 @@ import { registerKillCommands } from "./commands/kill.js";
|
|
|
21
23
|
import { registerAnalyticsCommands } from "./commands/analytics.js";
|
|
22
24
|
import { registerConfigCommands } from "./commands/config-cmd.js";
|
|
23
25
|
import { registerOnboardCommands } from "./commands/onboard.js";
|
|
26
|
+
import { registerOrgCommands } from "./commands/orgs.js";
|
|
27
|
+
import { registerActivityCommands } from "./commands/activity.js";
|
|
28
|
+
import { registerStatusCommand } from "./commands/status.js";
|
|
29
|
+
import { registerWatchCommand } from "./commands/watch.js";
|
|
30
|
+
import { registerProviderCommands } from "./commands/providers.js";
|
|
31
|
+
import { registerAgentGuardCommands } from "./commands/agent-guard.js";
|
|
32
|
+
import { CLI_VERSION } from "./version.js";
|
|
24
33
|
const program = new Command();
|
|
25
34
|
program
|
|
26
35
|
.name("kill-switch")
|
|
27
36
|
.description("Monitor cloud spending, kill runaway services, protect your infrastructure")
|
|
28
|
-
.version(
|
|
37
|
+
.version(CLI_VERSION)
|
|
29
38
|
.option("--json", "Output as JSON (for automation/scripting)")
|
|
30
39
|
.option("--api-key <key>", "API key (overrides config and env)")
|
|
31
|
-
.option("--api-url <url>", "API URL (overrides config and env)")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
.option("--api-url <url>", "API URL (overrides config and env)")
|
|
41
|
+
.option("-y, --yes", "Skip confirmation prompts");
|
|
42
|
+
/**
|
|
43
|
+
* Create an SDK client with the resolved apiKey/apiUrl.
|
|
44
|
+
* Called lazily when a command runs (after options are parsed).
|
|
45
|
+
*/
|
|
46
|
+
const createClient = () => {
|
|
47
|
+
const opts = program.opts();
|
|
48
|
+
return new KillSwitchClient({
|
|
49
|
+
apiKey: resolveApiKey(opts.apiKey),
|
|
50
|
+
baseUrl: resolveApiUrl(opts.apiUrl),
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
registerAuthCommands(program, createClient);
|
|
54
|
+
registerAccountCommands(program, createClient);
|
|
55
|
+
registerRuleCommands(program, createClient);
|
|
56
|
+
registerShieldCommands(program, createClient);
|
|
57
|
+
registerCheckCommands(program, createClient);
|
|
58
|
+
registerAlertCommands(program, createClient);
|
|
59
|
+
registerKillCommands(program, createClient);
|
|
60
|
+
registerAnalyticsCommands(program, createClient);
|
|
40
61
|
registerConfigCommands(program);
|
|
41
|
-
registerOnboardCommands(program);
|
|
62
|
+
registerOnboardCommands(program, createClient);
|
|
63
|
+
registerOrgCommands(program, createClient);
|
|
64
|
+
registerActivityCommands(program, createClient);
|
|
65
|
+
registerStatusCommand(program, createClient);
|
|
66
|
+
registerWatchCommand(program, createClient);
|
|
67
|
+
registerProviderCommands(program, createClient);
|
|
68
|
+
registerAgentGuardCommands(program);
|
|
42
69
|
program.parse();
|
package/dist/output.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Output formatting — tables for humans, JSON for machines
|
|
3
|
+
*
|
|
4
|
+
* Colors via chalk, spinners via ora.
|
|
5
|
+
* Respects NO_COLOR env var and --json flag.
|
|
3
6
|
*/
|
|
7
|
+
import { type Ora } from "ora";
|
|
8
|
+
declare const c: {
|
|
9
|
+
bold: ((s: string) => string) | import("chalk").ChalkInstance;
|
|
10
|
+
dim: import("chalk").ChalkInstance | ((s: string) => string);
|
|
11
|
+
green: import("chalk").ChalkInstance | ((s: string) => string);
|
|
12
|
+
red: import("chalk").ChalkInstance | ((s: string) => string);
|
|
13
|
+
yellow: import("chalk").ChalkInstance | ((s: string) => string);
|
|
14
|
+
cyan: import("chalk").ChalkInstance | ((s: string) => string);
|
|
15
|
+
};
|
|
16
|
+
export { c as colors };
|
|
4
17
|
export interface Column {
|
|
5
18
|
key: string;
|
|
6
19
|
header: string;
|
|
@@ -11,8 +24,17 @@ export declare function formatObject(obj: any, fields?: string[]): void;
|
|
|
11
24
|
export declare function outputJson(data: any): void;
|
|
12
25
|
export declare function outputError(message: string, json: boolean): void;
|
|
13
26
|
/**
|
|
14
|
-
*
|
|
27
|
+
* Map SDK errors to contextual CLI messages.
|
|
15
28
|
*/
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
export declare function formatSdkError(err: unknown): {
|
|
30
|
+
message: string;
|
|
31
|
+
exitCode: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a command handler with error handling, spinner, and JSON support.
|
|
35
|
+
*/
|
|
36
|
+
export declare function handleError(err: unknown, json: boolean): never;
|
|
37
|
+
export declare function spinner(text: string): Ora;
|
|
38
|
+
export declare function success(msg: string): void;
|
|
39
|
+
export declare function warn(msg: string): void;
|
|
40
|
+
export declare function fail(msg: string): void;
|
package/dist/output.js
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Output formatting — tables for humans, JSON for machines
|
|
3
|
+
*
|
|
4
|
+
* Colors via chalk, spinners via ora.
|
|
5
|
+
* Respects NO_COLOR env var and --json flag.
|
|
3
6
|
*/
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import { ApiError, AuthenticationError, ForbiddenError, NotFoundError, RateLimitError, NetworkError, TimeoutError } from "@kill-switch/sdk";
|
|
10
|
+
const noColor = !!process.env.NO_COLOR;
|
|
11
|
+
// Color helpers — no-op when NO_COLOR is set
|
|
12
|
+
const c = {
|
|
13
|
+
bold: noColor ? (s) => s : chalk.bold,
|
|
14
|
+
dim: noColor ? (s) => s : chalk.dim,
|
|
15
|
+
green: noColor ? (s) => s : chalk.green,
|
|
16
|
+
red: noColor ? (s) => s : chalk.red,
|
|
17
|
+
yellow: noColor ? (s) => s : chalk.yellow,
|
|
18
|
+
cyan: noColor ? (s) => s : chalk.cyan,
|
|
19
|
+
};
|
|
20
|
+
export { c as colors };
|
|
4
21
|
export function formatTable(rows, columns) {
|
|
5
22
|
if (rows.length === 0) {
|
|
6
|
-
console.log("No results.");
|
|
23
|
+
console.log(c.dim("No results."));
|
|
7
24
|
return;
|
|
8
25
|
}
|
|
9
26
|
// Calculate column widths
|
|
@@ -17,14 +34,33 @@ export function formatTable(rows, columns) {
|
|
|
17
34
|
});
|
|
18
35
|
// Header
|
|
19
36
|
const headerLine = columns
|
|
20
|
-
.map((col, i) => col.header.padEnd(widths[i]))
|
|
37
|
+
.map((col, i) => c.bold(col.header.padEnd(widths[i])))
|
|
21
38
|
.join(" ");
|
|
22
39
|
console.log(headerLine);
|
|
23
|
-
console.log(widths.map((w) => "
|
|
40
|
+
console.log(c.dim(widths.map((w) => "─".repeat(w)).join(" ")));
|
|
24
41
|
// Rows
|
|
25
42
|
for (const row of rows) {
|
|
26
43
|
const line = columns
|
|
27
|
-
.map((col, i) =>
|
|
44
|
+
.map((col, i) => {
|
|
45
|
+
const val = String(row[col.key] ?? "");
|
|
46
|
+
const display = val.padEnd(widths[i]).slice(0, widths[i]);
|
|
47
|
+
// Color status-like values
|
|
48
|
+
if (col.key === "status" || col.key === "enabled") {
|
|
49
|
+
if (val === "active" || val === "true")
|
|
50
|
+
return c.green(display);
|
|
51
|
+
if (val === "paused" || val === "false" || val === "disabled")
|
|
52
|
+
return c.yellow(display);
|
|
53
|
+
if (val === "disconnected" || val === "error")
|
|
54
|
+
return c.red(display);
|
|
55
|
+
}
|
|
56
|
+
if (col.key === "severity") {
|
|
57
|
+
if (val === "critical")
|
|
58
|
+
return c.red(display);
|
|
59
|
+
if (val === "warning")
|
|
60
|
+
return c.yellow(display);
|
|
61
|
+
}
|
|
62
|
+
return display;
|
|
63
|
+
})
|
|
28
64
|
.join(" ");
|
|
29
65
|
console.log(line);
|
|
30
66
|
}
|
|
@@ -35,7 +71,7 @@ export function formatObject(obj, fields) {
|
|
|
35
71
|
for (const key of keys) {
|
|
36
72
|
const val = obj[key];
|
|
37
73
|
const display = typeof val === "object" ? JSON.stringify(val) : String(val ?? "");
|
|
38
|
-
console.log(`${key.padEnd(maxKeyLen + 2)}${display}`);
|
|
74
|
+
console.log(`${c.bold(key.padEnd(maxKeyLen + 2))}${display}`);
|
|
39
75
|
}
|
|
40
76
|
}
|
|
41
77
|
export function outputJson(data) {
|
|
@@ -46,15 +82,68 @@ export function outputError(message, json) {
|
|
|
46
82
|
console.error(JSON.stringify({ error: message }));
|
|
47
83
|
}
|
|
48
84
|
else {
|
|
49
|
-
console.error(
|
|
85
|
+
console.error(`${c.red("Error:")} ${message}`);
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
/**
|
|
53
|
-
*
|
|
89
|
+
* Map SDK errors to contextual CLI messages.
|
|
54
90
|
*/
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
export function formatSdkError(err) {
|
|
92
|
+
if (err instanceof AuthenticationError) {
|
|
93
|
+
return {
|
|
94
|
+
message: "Authentication failed. Run `ks auth login` or set KILL_SWITCH_API_KEY.",
|
|
95
|
+
exitCode: 2,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (err instanceof ForbiddenError) {
|
|
99
|
+
const tier = err.tierInfo;
|
|
100
|
+
if (tier) {
|
|
101
|
+
return {
|
|
102
|
+
message: `Requires ${tier.currentTier ? `upgrade from ${tier.currentTier}` : "a higher"} plan.${tier.upgradeUrl ? ` Upgrade: https://app.kill-switch.net${tier.upgradeUrl}` : ""}`,
|
|
103
|
+
exitCode: 1,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { message: err.message, exitCode: 2 };
|
|
107
|
+
}
|
|
108
|
+
if (err instanceof NotFoundError) {
|
|
109
|
+
return { message: `${err.message} Run \`ks accounts list\` or \`ks rules list\` to see available resources.`, exitCode: 1 };
|
|
110
|
+
}
|
|
111
|
+
if (err instanceof RateLimitError) {
|
|
112
|
+
return { message: `Rate limited. Try again in ${err.retryAfter}s.`, exitCode: 1 };
|
|
113
|
+
}
|
|
114
|
+
if (err instanceof NetworkError) {
|
|
115
|
+
return { message: err.message, exitCode: 1 };
|
|
116
|
+
}
|
|
117
|
+
if (err instanceof TimeoutError) {
|
|
118
|
+
return { message: err.message, exitCode: 1 };
|
|
119
|
+
}
|
|
120
|
+
if (err instanceof ApiError) {
|
|
121
|
+
return { message: err.message, exitCode: 1 };
|
|
122
|
+
}
|
|
123
|
+
if (err instanceof Error) {
|
|
124
|
+
return { message: err.message, exitCode: 1 };
|
|
125
|
+
}
|
|
126
|
+
return { message: String(err), exitCode: 1 };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Wrap a command handler with error handling, spinner, and JSON support.
|
|
130
|
+
*/
|
|
131
|
+
export function handleError(err, json) {
|
|
132
|
+
const { message, exitCode } = formatSdkError(err);
|
|
133
|
+
outputError(message, json);
|
|
134
|
+
process.exit(exitCode);
|
|
135
|
+
}
|
|
136
|
+
// ─── Spinner helpers ─────────────────────────────────────────────────────────
|
|
137
|
+
export function spinner(text) {
|
|
138
|
+
return ora({ text, isSilent: !!process.env.NO_COLOR });
|
|
139
|
+
}
|
|
140
|
+
// ─── Status indicators ──────────────────────────────────────────────────────
|
|
141
|
+
export function success(msg) {
|
|
142
|
+
console.log(`${c.green("✓")} ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
export function warn(msg) {
|
|
145
|
+
console.log(`${c.yellow("⚠")} ${msg}`);
|
|
146
|
+
}
|
|
147
|
+
export function fail(msg) {
|
|
148
|
+
console.log(`${c.red("✗")} ${msg}`);
|
|
60
149
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts — shared across commands
|
|
3
|
+
*/
|
|
4
|
+
export declare function ask(question: string): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Ask for yes/no confirmation.
|
|
7
|
+
* Returns true if user confirms. Skips prompt and returns true if
|
|
8
|
+
* `--yes` flag is set or `--json` mode is active.
|
|
9
|
+
*/
|
|
10
|
+
export declare function confirm(message: string, opts?: {
|
|
11
|
+
yes?: boolean;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
}): Promise<boolean>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts — shared across commands
|
|
3
|
+
*/
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
export function ask(question) {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
rl.question(question, (answer) => {
|
|
9
|
+
rl.close();
|
|
10
|
+
resolve(answer.trim());
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Ask for yes/no confirmation.
|
|
16
|
+
* Returns true if user confirms. Skips prompt and returns true if
|
|
17
|
+
* `--yes` flag is set or `--json` mode is active.
|
|
18
|
+
*/
|
|
19
|
+
export async function confirm(message, opts = {}) {
|
|
20
|
+
if (opts.yes || opts.json)
|
|
21
|
+
return true;
|
|
22
|
+
const answer = await ask(`${message} (y/N): `);
|
|
23
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
24
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CLI_VERSION: string;
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kill-switch/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Kill Switch CLI — monitor cloud spending, kill runaway services from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,21 +13,43 @@
|
|
|
13
13
|
"test": "vitest run"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"
|
|
16
|
+
"@kill-switch/agent-guard": "^0.1.0",
|
|
17
|
+
"@kill-switch/sdk": "^0.1.0",
|
|
18
|
+
"chalk": "^5.6.2",
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"ora": "^8.2.0"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
|
-
"
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
20
24
|
"tsx": "^4.7.0",
|
|
21
|
-
"
|
|
25
|
+
"typescript": "^5.4.0",
|
|
26
|
+
"vite": "^6.4.1",
|
|
27
|
+
"vitest": "^3.2.4"
|
|
22
28
|
},
|
|
23
29
|
"engines": {
|
|
24
30
|
"node": ">=18"
|
|
25
31
|
},
|
|
26
|
-
"files": [
|
|
27
|
-
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"keywords": [
|
|
37
|
+
"cloud",
|
|
38
|
+
"kill-switch",
|
|
39
|
+
"cost-monitoring",
|
|
40
|
+
"cloudflare",
|
|
41
|
+
"aws",
|
|
42
|
+
"gcp",
|
|
43
|
+
"runpod",
|
|
44
|
+
"gpu",
|
|
45
|
+
"finops",
|
|
46
|
+
"billing",
|
|
47
|
+
"cli",
|
|
48
|
+
"devops"
|
|
49
|
+
],
|
|
28
50
|
"repository": {
|
|
29
51
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/
|
|
52
|
+
"url": "https://github.com/divinci-ai/kill-switch",
|
|
31
53
|
"directory": "packages/cli"
|
|
32
54
|
},
|
|
33
55
|
"homepage": "https://kill-switch.net",
|