@supalytics/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,83 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { addSite, CONFIG_FILE, getConfig, setDefaultSite } from "../config";
4
+ import { mkdir } from "fs/promises";
5
+ import { dirname } from "path";
6
+
7
+ const API_BASE = process.env.SUPALYTICS_API_URL || "http://localhost:3000";
8
+
9
+ interface VerifyResponse {
10
+ domain: string;
11
+ }
12
+
13
+ async function verifyApiKey(apiKey: string): Promise<VerifyResponse | null> {
14
+ try {
15
+ // Use a minimal analytics query to verify the key and get the domain
16
+ const response = await fetch(`${API_BASE}/v1/analytics`, {
17
+ method: "POST",
18
+ headers: {
19
+ Authorization: `Bearer ${apiKey}`,
20
+ "Content-Type": "application/json",
21
+ },
22
+ body: JSON.stringify({
23
+ metrics: ["pageviews"],
24
+ date_range: "7d",
25
+ limit: 1,
26
+ }),
27
+ });
28
+
29
+ if (!response.ok) {
30
+ return null;
31
+ }
32
+
33
+ const data = await response.json();
34
+ return { domain: data.meta.domain };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export const loginCommand = new Command("login")
41
+ .description("Add a site by verifying its API key. Can be called multiple times to add multiple sites.")
42
+ .argument("<api-key>", "Your API key (starts with sly_)")
43
+ .option("--default", "Set this site as the default")
44
+ .action(async (apiKey: string, options: { default?: boolean }) => {
45
+ apiKey = apiKey.trim();
46
+
47
+ // Validate API key format
48
+ if (!apiKey.startsWith("sly_")) {
49
+ console.error(chalk.red("Error: Invalid API key format. Keys start with 'sly_'"));
50
+ process.exit(1);
51
+ }
52
+
53
+ // Verify API key with server
54
+ console.log(chalk.dim("Verifying API key..."));
55
+ const result = await verifyApiKey(apiKey);
56
+
57
+ if (!result) {
58
+ console.error(chalk.red("Error: Invalid API key or unable to verify"));
59
+ process.exit(1);
60
+ }
61
+
62
+ // Ensure config directory exists
63
+ await mkdir(dirname(CONFIG_FILE), { recursive: true });
64
+
65
+ // Add site to config
66
+ await addSite(result.domain, apiKey);
67
+
68
+ // Set as default if requested
69
+ if (options.default) {
70
+ await setDefaultSite(result.domain);
71
+ }
72
+
73
+ const config = await getConfig();
74
+ const isDefault = config.defaultSite === result.domain;
75
+ const siteCount = Object.keys(config.sites).length;
76
+
77
+ console.log(chalk.green(`✓ Added ${result.domain}`) + (isDefault ? chalk.dim(" (default)") : ""));
78
+ console.log(chalk.dim(` ${siteCount} site${siteCount > 1 ? "s" : ""} configured`));
79
+
80
+ if (siteCount > 1 && !isDefault) {
81
+ console.log(chalk.dim(` Use --default flag or 'supalytics default ${result.domain}' to set as default`));
82
+ }
83
+ });
@@ -0,0 +1,15 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { CONFIG_FILE } from "../config";
4
+ import { unlink } from "fs/promises";
5
+
6
+ export const logoutCommand = new Command("logout")
7
+ .description("Remove stored credentials")
8
+ .action(async () => {
9
+ try {
10
+ await unlink(CONFIG_FILE);
11
+ console.log(chalk.green("✓ Logged out successfully"));
12
+ } catch {
13
+ console.log(chalk.dim("Already logged out"));
14
+ }
15
+ });
@@ -0,0 +1,93 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { query, formatNumber } from "../api";
4
+ import { getDefaultSite } from "../config";
5
+ import { sparkBar, truncate, formatRevenue } from "../ui";
6
+
7
+ export const pagesCommand = new Command("pages")
8
+ .description("Top pages by visitors")
9
+ .option("-s, --site <site>", "Site to query")
10
+ .option("-p, --period <period>", "Time period (7d, 30d, 90d, 12mo, all)", "30d")
11
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
12
+ .option("--end <date>", "End date (YYYY-MM-DD)")
13
+ .option("-l, --limit <number>", "Number of results", "10")
14
+ .option("-f, --filter <filters...>", "Filters in format 'field:operator:value'")
15
+ .option("--no-revenue", "Exclude revenue metrics")
16
+ .option("--json", "Output as JSON")
17
+ .action(async (options) => {
18
+ const site = options.site || (await getDefaultSite());
19
+
20
+ if (!site) {
21
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
22
+ process.exit(1);
23
+ }
24
+
25
+ const dateRange = options.start && options.end
26
+ ? [options.start, options.end] as [string, string]
27
+ : options.period;
28
+
29
+ const filters = options.filter
30
+ ? options.filter.map((f: string) => {
31
+ const parts = f.split(":");
32
+ if (parts.length < 3) {
33
+ console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`));
34
+ process.exit(1);
35
+ }
36
+ const [field, operator, ...valueParts] = parts;
37
+ return [field, operator, valueParts.join(":")] as [string, string, string];
38
+ })
39
+ : undefined;
40
+
41
+ try {
42
+ const metrics = ["visitors"];
43
+ if (options.revenue !== false) {
44
+ metrics.push("revenue");
45
+ }
46
+
47
+ const response = await query(site, {
48
+ metrics,
49
+ dimensions: ["page"],
50
+ filters,
51
+ date_range: dateRange,
52
+ limit: parseInt(options.limit),
53
+ include_revenue: options.revenue !== false,
54
+ });
55
+
56
+ if (options.json) {
57
+ console.log(JSON.stringify(response, null, 2));
58
+ return;
59
+ }
60
+
61
+ const [startDate, endDate] = response.meta.date_range;
62
+
63
+ console.log();
64
+ console.log(chalk.bold(" 📄 Top Pages"), chalk.dim(`${startDate} → ${endDate}`));
65
+ console.log();
66
+
67
+ if (response.data.length === 0) {
68
+ console.log(chalk.dim(" No data"));
69
+ console.log();
70
+ return;
71
+ }
72
+
73
+ const maxVisitors = Math.max(...response.data.map(r => r.metrics.visitors || 0), 1);
74
+
75
+ for (const result of response.data) {
76
+ const page = truncate(result.dimensions?.page || "/", 32);
77
+ const visitors = result.metrics.visitors || 0;
78
+ const bar = sparkBar(visitors, maxVisitors);
79
+
80
+ let line = ` ${page.padEnd(34)} ${bar} ${formatNumber(visitors).padStart(6)}`;
81
+
82
+ if (options.revenue !== false && result.metrics.revenue != null) {
83
+ line += ` ${formatRevenue(result.metrics.revenue)}`;
84
+ }
85
+
86
+ console.log(line);
87
+ }
88
+ console.log();
89
+ } catch (error) {
90
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
91
+ process.exit(1);
92
+ }
93
+ });
@@ -0,0 +1,162 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { query, formatNumber, formatPercent, formatDuration } from "../api";
4
+ import { getDefaultSite } from "../config";
5
+
6
+ const queryDescription = `Flexible query with custom metrics and dimensions.
7
+
8
+ Examples:
9
+ # Top pages with revenue
10
+ supalytics query -d page -m visitors,revenue
11
+
12
+ # Traffic by country and device
13
+ supalytics query -d country,device -m visitors
14
+
15
+ # Blog traffic from US
16
+ supalytics query -d page -f "page:contains:/blog" -f "country:is:US"
17
+
18
+ # Hourly breakdown for last 7 days
19
+ supalytics query -d hour -m visitors -p 7d
20
+
21
+ # UTM campaign performance
22
+ supalytics query -d utm_source,utm_campaign -m visitors,revenue
23
+
24
+ # Sort by revenue
25
+ supalytics query -d page --sort revenue:desc
26
+
27
+ # List all custom events
28
+ supalytics query -d event
29
+
30
+ # Top countries for visitors who signed up
31
+ supalytics query -d country -f "event:is:signup"
32
+
33
+ # Pages visited by premium users
34
+ supalytics query -d page -f "event_property:is:plan:premium"`;
35
+
36
+ export const queryCommand = new Command("query")
37
+ .description(queryDescription)
38
+ .option("-s, --site <site>", "Site to query")
39
+ .option("-m, --metrics <metrics>", "Metrics: visitors, bounce_rate, avg_session_duration, revenue, conversions", "visitors")
40
+ .option("-d, --dimensions <dimensions>", "Dimensions: page, referrer, country, region, city, browser, os, device, date, hour, event, utm_* (max 2, event cannot combine)")
41
+ .option("-f, --filter <filters...>", "Filters: field:operator:value (e.g., 'page:contains:/blog', 'event:is:signup', 'event_property:is:plan:premium')")
42
+ .option("--sort <sort>", "Sort by field:order (e.g., 'revenue:desc', 'visitors:asc')")
43
+ .option("--timezone <tz>", "Timezone for date grouping (e.g., 'America/New_York')", "UTC")
44
+ .option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
45
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
46
+ .option("--end <date>", "End date (YYYY-MM-DD)")
47
+ .option("-l, --limit <number>", "Number of results (1-1000)", "10")
48
+ .option("--offset <number>", "Skip results for pagination", "0")
49
+ .option("--no-revenue", "Exclude revenue metrics")
50
+ .option("--json", "Output as JSON")
51
+ .action(async (options) => {
52
+ const site = options.site || (await getDefaultSite());
53
+
54
+ if (!site) {
55
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
56
+ process.exit(1);
57
+ }
58
+
59
+ const dateRange = options.start && options.end
60
+ ? [options.start, options.end] as [string, string]
61
+ : options.period;
62
+
63
+ // Parse metrics
64
+ const metrics = options.metrics.split(",").map((m: string) => m.trim());
65
+
66
+ // Parse dimensions
67
+ const dimensions = options.dimensions
68
+ ? options.dimensions.split(",").map((d: string) => d.trim())
69
+ : undefined;
70
+
71
+ // Parse filters - NEW FORMAT: field:operator:value
72
+ const filters = options.filter
73
+ ? options.filter.map((f: string) => {
74
+ const parts = f.split(":");
75
+ if (parts.length < 3) {
76
+ console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`));
77
+ process.exit(1);
78
+ }
79
+ const [field, operator, ...valueParts] = parts;
80
+ return [field, operator, valueParts.join(":")] as [string, string, string];
81
+ })
82
+ : undefined;
83
+
84
+ // Parse sort
85
+ const sort = options.sort
86
+ ? (() => {
87
+ const [field, order] = options.sort.split(":");
88
+ if (!field || !order || (order !== "asc" && order !== "desc")) {
89
+ console.error(chalk.red(`Invalid sort format: ${options.sort}. Use 'field:order' where order is 'asc' or 'desc'`));
90
+ process.exit(1);
91
+ }
92
+ return { field, order: order as "asc" | "desc" };
93
+ })()
94
+ : undefined;
95
+
96
+ try {
97
+ const response = await query(site, {
98
+ metrics,
99
+ dimensions,
100
+ filters,
101
+ sort,
102
+ timezone: options.timezone,
103
+ date_range: dateRange,
104
+ limit: parseInt(options.limit),
105
+ offset: parseInt(options.offset),
106
+ include_revenue: options.revenue !== false,
107
+ });
108
+
109
+ if (options.json) {
110
+ console.log(JSON.stringify(response, null, 2));
111
+ return;
112
+ }
113
+
114
+ const [startDate, endDate] = response.meta.date_range;
115
+
116
+ console.log();
117
+ console.log(chalk.bold("Query Results"), chalk.dim(`${startDate} → ${endDate}`));
118
+ console.log(chalk.dim(` ${response.data.length} rows in ${response.meta.query_ms}ms`));
119
+ if (response.pagination.has_more) {
120
+ console.log(chalk.dim(` More results available (offset: ${response.pagination.offset + response.pagination.limit})`));
121
+ }
122
+ console.log();
123
+
124
+ if (response.data.length === 0) {
125
+ console.log(chalk.dim(" No data"));
126
+ return;
127
+ }
128
+
129
+ // Display results
130
+ for (const result of response.data) {
131
+ const parts: string[] = [];
132
+
133
+ // Dimensions
134
+ if (result.dimensions) {
135
+ for (const [key, value] of Object.entries(result.dimensions)) {
136
+ parts.push(`${chalk.dim(key)}=${value}`);
137
+ }
138
+ }
139
+
140
+ // Metrics
141
+ for (const [key, value] of Object.entries(result.metrics)) {
142
+ let formatted: string;
143
+ if (key === "bounce_rate" || key === "conversion_rate") {
144
+ formatted = formatPercent(value);
145
+ } else if (key === "avg_session_duration") {
146
+ formatted = formatDuration(value);
147
+ } else if (key === "revenue") {
148
+ formatted = chalk.green("$" + (value / 100).toFixed(2));
149
+ } else {
150
+ formatted = formatNumber(value);
151
+ }
152
+ parts.push(`${chalk.dim(key)}=${formatted}`);
153
+ }
154
+
155
+ console.log(` ${parts.join(" ")}`);
156
+ }
157
+ console.log();
158
+ } catch (error) {
159
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
160
+ process.exit(1);
161
+ }
162
+ });
@@ -0,0 +1,110 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getRealtime } from "../api";
4
+ import { getDefaultSite } from "../config";
5
+ import { sparkBar, truncate, logo } from "../ui";
6
+
7
+ export const realtimeCommand = new Command("realtime")
8
+ .description("Live visitors on your site right now")
9
+ .option("-s, --site <site>", "Site to query")
10
+ .option("--json", "Output as JSON")
11
+ .option("-w, --watch", "Auto-refresh every 30 seconds")
12
+ .action(async (options) => {
13
+ const site = options.site || (await getDefaultSite());
14
+
15
+ if (!site) {
16
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
17
+ process.exit(1);
18
+ }
19
+
20
+ const display = async () => {
21
+ try {
22
+ const response = await getRealtime(site);
23
+
24
+ if (options.json) {
25
+ console.log(JSON.stringify(response, null, 2));
26
+ return;
27
+ }
28
+
29
+ // Clear screen in watch mode
30
+ if (options.watch) {
31
+ console.clear();
32
+ }
33
+
34
+ const { data, meta } = response;
35
+ const time = new Date(meta.timestamp).toLocaleTimeString();
36
+ const maxVisitors = Math.max(
37
+ ...data.pages.map(p => p.visitors),
38
+ ...data.countries.map(c => c.visitors),
39
+ ...data.referrers.map(r => r.visitors),
40
+ 1
41
+ );
42
+
43
+ // Logo
44
+ console.log(logo());
45
+
46
+ // Site info
47
+ console.log(chalk.bold(` ${site}`));
48
+ console.log(chalk.dim(` Updated ${time}`));
49
+ console.log();
50
+
51
+ // Big visitor count
52
+ const visitorCount = String(data.active_visitors);
53
+ console.log(` ${chalk.green.bold(visitorCount)} ${chalk.dim("visitors right now")}`);
54
+ console.log();
55
+
56
+ // Top pages
57
+ if (data.pages.length > 0) {
58
+ console.log(chalk.dim(" PAGES"));
59
+ for (const p of data.pages.slice(0, 5)) {
60
+ const page = truncate(p.page, 36);
61
+ const bar = sparkBar(p.visitors, maxVisitors, 12);
62
+ console.log(` ${page.padEnd(38)} ${bar} ${chalk.bold(String(p.visitors).padStart(3))}`);
63
+ }
64
+ console.log();
65
+ }
66
+
67
+ // Countries
68
+ if (data.countries.length > 0) {
69
+ console.log(chalk.dim(" COUNTRIES"));
70
+ for (const c of data.countries.slice(0, 5)) {
71
+ const bar = sparkBar(c.visitors, maxVisitors, 12);
72
+ console.log(` ${c.country.padEnd(38)} ${bar} ${chalk.bold(String(c.visitors).padStart(3))}`);
73
+ }
74
+ console.log();
75
+ }
76
+
77
+ // Referrers
78
+ if (data.referrers.length > 0) {
79
+ console.log(chalk.dim(" REFERRERS"));
80
+ for (const r of data.referrers.slice(0, 5)) {
81
+ const ref = truncate(r.referrer || "Direct", 36);
82
+ const bar = sparkBar(r.visitors, maxVisitors, 12);
83
+ console.log(` ${ref.padEnd(38)} ${bar} ${chalk.bold(String(r.visitors).padStart(3))}`);
84
+ }
85
+ console.log();
86
+ }
87
+
88
+ if (data.pages.length === 0 && data.countries.length === 0 && data.referrers.length === 0) {
89
+ console.log(chalk.dim(" No active visitors right now"));
90
+ console.log();
91
+ }
92
+
93
+ if (options.watch) {
94
+ console.log(chalk.dim(" ──────────────────────────────────────────────────────"));
95
+ console.log(chalk.dim(" Refreshing every 30s • Press Ctrl+C to exit"));
96
+ }
97
+ } catch (error) {
98
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
99
+ if (!options.watch) {
100
+ process.exit(1);
101
+ }
102
+ }
103
+ };
104
+
105
+ await display();
106
+
107
+ if (options.watch) {
108
+ setInterval(display, 30000);
109
+ }
110
+ });
@@ -0,0 +1,95 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { query, formatNumber } from "../api";
4
+ import { getDefaultSite } from "../config";
5
+ import { sparkBar, truncate, formatRevenue } from "../ui";
6
+
7
+ export const referrersCommand = new Command("referrers")
8
+ .description("Top referrers")
9
+ .option("-s, --site <site>", "Site to query")
10
+ .option("-p, --period <period>", "Time period (7d, 30d, 90d, 12mo, all)", "30d")
11
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
12
+ .option("--end <date>", "End date (YYYY-MM-DD)")
13
+ .option("-l, --limit <number>", "Number of results", "10")
14
+ .option("-f, --filter <filters...>", "Filters in format 'field:operator:value'")
15
+ .option("--no-revenue", "Exclude revenue metrics")
16
+ .option("--json", "Output as JSON")
17
+ .action(async (options) => {
18
+ const site = options.site || (await getDefaultSite());
19
+
20
+ if (!site) {
21
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
22
+ process.exit(1);
23
+ }
24
+
25
+ const dateRange = options.start && options.end
26
+ ? [options.start, options.end] as [string, string]
27
+ : options.period;
28
+
29
+ const filters = options.filter
30
+ ? options.filter.map((f: string) => {
31
+ const parts = f.split(":");
32
+ if (parts.length < 3) {
33
+ console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`));
34
+ process.exit(1);
35
+ }
36
+ const [field, operator, ...valueParts] = parts;
37
+ return [field, operator, valueParts.join(":")] as [string, string, string];
38
+ })
39
+ : undefined;
40
+
41
+ try {
42
+ const metrics = ["visitors"];
43
+ if (options.revenue !== false) {
44
+ metrics.push("revenue");
45
+ }
46
+
47
+ const response = await query(site, {
48
+ metrics,
49
+ dimensions: ["referrer"],
50
+ filters,
51
+ date_range: dateRange,
52
+ limit: parseInt(options.limit),
53
+ include_revenue: options.revenue !== false,
54
+ });
55
+
56
+ if (options.json) {
57
+ console.log(JSON.stringify(response, null, 2));
58
+ return;
59
+ }
60
+
61
+ const [startDate, endDate] = response.meta.date_range;
62
+
63
+ console.log();
64
+ console.log(chalk.bold(" 🔗 Referrers"), chalk.dim(`${startDate} → ${endDate}`));
65
+ console.log();
66
+
67
+ if (response.data.length === 0) {
68
+ console.log(chalk.dim(" No data"));
69
+ console.log();
70
+ return;
71
+ }
72
+
73
+ const maxVisitors = Math.max(...response.data.map(r => r.metrics.visitors || 0), 1);
74
+
75
+ for (const result of response.data) {
76
+ const referrer = result.dimensions?.referrer || "";
77
+ const displayRef = referrer === "" ? "Direct / None" : referrer;
78
+ const source = truncate(displayRef, 32);
79
+ const visitors = result.metrics.visitors || 0;
80
+ const bar = sparkBar(visitors, maxVisitors);
81
+
82
+ let line = ` ${source.padEnd(34)} ${bar} ${formatNumber(visitors).padStart(6)}`;
83
+
84
+ if (options.revenue !== false && result.metrics.revenue != null) {
85
+ line += ` ${formatRevenue(result.metrics.revenue)}`;
86
+ }
87
+
88
+ console.log(line);
89
+ }
90
+ console.log();
91
+ } catch (error) {
92
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
93
+ process.exit(1);
94
+ }
95
+ });
@@ -0,0 +1,65 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getConfig, removeSite, setDefaultSite } from "../config";
4
+
5
+ export const sitesCommand = new Command("sites")
6
+ .description("List all configured sites")
7
+ .action(async () => {
8
+ const config = await getConfig();
9
+ const sites = Object.keys(config.sites);
10
+
11
+ if (sites.length === 0) {
12
+ console.log(chalk.dim("No sites configured. Run `supalytics login <api-key>` to add a site."));
13
+ return;
14
+ }
15
+
16
+ console.log();
17
+ console.log(chalk.bold("Configured Sites"));
18
+ console.log();
19
+
20
+ for (const site of sites) {
21
+ const isDefault = config.defaultSite === site;
22
+ const apiKey = config.sites[site].apiKey;
23
+ const maskedKey = apiKey.slice(0, 8) + "..." + apiKey.slice(-4);
24
+
25
+ if (isDefault) {
26
+ console.log(` ${chalk.green("●")} ${site} ${chalk.dim("(default)")} ${chalk.dim(maskedKey)}`);
27
+ } else {
28
+ console.log(` ${chalk.dim("○")} ${site} ${chalk.dim(maskedKey)}`);
29
+ }
30
+ }
31
+ console.log();
32
+ });
33
+
34
+ export const defaultCommand = new Command("default")
35
+ .description("Set the default site")
36
+ .argument("<domain>", "Domain to set as default")
37
+ .action(async (domain: string) => {
38
+ const success = await setDefaultSite(domain);
39
+
40
+ if (!success) {
41
+ const config = await getConfig();
42
+ const sites = Object.keys(config.sites);
43
+ console.error(chalk.red(`Error: Site '${domain}' not found.`));
44
+ if (sites.length > 0) {
45
+ console.log(chalk.dim(`Available sites: ${sites.join(", ")}`));
46
+ }
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(chalk.green(`✓ Default site set to ${domain}`));
51
+ });
52
+
53
+ export const removeCommand = new Command("remove")
54
+ .description("Remove a site")
55
+ .argument("<domain>", "Domain to remove")
56
+ .action(async (domain: string) => {
57
+ const success = await removeSite(domain);
58
+
59
+ if (!success) {
60
+ console.error(chalk.red(`Error: Site '${domain}' not found.`));
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log(chalk.green(`✓ Removed ${domain}`));
65
+ });