@supalytics/cli 0.4.1 → 0.4.3

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.
@@ -1,251 +1,255 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
1
+ import chalk from "chalk"
2
+ import { Command } from "commander"
3
3
  import {
4
- getConfig,
5
- removeSite,
6
- setDefaultSite,
7
- getAuth,
8
- addSiteWithId,
9
- saveConfig,
10
- } from "../config";
4
+ addSiteWithId,
5
+ getAuth,
6
+ getConfig,
7
+ removeSite,
8
+ saveConfig,
9
+ setDefaultSite,
10
+ } from "../config"
11
11
 
12
- const WEB_BASE = process.env.SUPALYTICS_WEB_URL || "https://www.supalytics.co";
12
+ const WEB_BASE = process.env.SUPALYTICS_WEB_URL || "https://www.supalytics.co"
13
13
 
14
14
  interface CreateSiteResponse {
15
- site: {
16
- id: string;
17
- site_id: string;
18
- domain: string;
19
- };
15
+ site: {
16
+ id: string
17
+ site_id: string
18
+ domain: string
19
+ }
20
20
  }
21
21
 
22
22
  interface UpdateSiteResponse {
23
- site: {
24
- id: string;
25
- site_id: string;
26
- domain: string;
27
- };
23
+ site: {
24
+ id: string
25
+ site_id: string
26
+ domain: string
27
+ }
28
28
  }
29
29
 
30
30
  export async function createSiteViaApi(
31
- accessToken: string,
32
- identifier: string
31
+ accessToken: string,
32
+ identifier: string
33
33
  ): Promise<CreateSiteResponse> {
34
- let response: Response;
35
- try {
36
- response = await fetch(`${WEB_BASE}/api/cli/sites`, {
37
- method: "POST",
38
- headers: {
39
- Authorization: `Bearer ${accessToken}`,
40
- "Content-Type": "application/json",
41
- },
42
- body: JSON.stringify({ identifier }),
43
- });
44
- } catch {
45
- throw new Error(`Network error: Could not connect to ${WEB_BASE}`);
46
- }
47
-
48
- if (!response.ok) {
49
- let errorMessage = "Failed to create site";
50
- try {
51
- const error = await response.json();
52
- errorMessage = error.error || errorMessage;
53
- } catch {
54
- // Response wasn't JSON
55
- }
56
- throw new Error(errorMessage);
57
- }
58
-
59
- return response.json();
34
+ let response: Response
35
+ try {
36
+ response = await fetch(`${WEB_BASE}/api/cli/sites`, {
37
+ method: "POST",
38
+ headers: {
39
+ Authorization: `Bearer ${accessToken}`,
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify({ identifier }),
43
+ })
44
+ } catch {
45
+ throw new Error(`Network error: Could not connect to ${WEB_BASE}`)
46
+ }
47
+
48
+ if (!response.ok) {
49
+ let errorMessage = "Failed to create site"
50
+ try {
51
+ const error = await response.json()
52
+ errorMessage = error.error || errorMessage
53
+ } catch {
54
+ // Response wasn't JSON
55
+ }
56
+ throw new Error(errorMessage)
57
+ }
58
+
59
+ return response.json()
60
60
  }
61
61
 
62
62
  export const sitesCommand = new Command("sites")
63
- .description("List all configured sites")
64
- .action(async () => {
65
- const config = await getConfig();
66
- const sites = Object.keys(config.sites);
67
-
68
- if (sites.length === 0) {
69
- console.log(chalk.dim("No sites configured."));
70
- console.log(chalk.dim("Run `supalytics login` then `supalytics sites add <name>` to add a site."));
71
- return;
72
- }
73
-
74
- console.log();
75
- console.log(chalk.bold("Configured Sites"));
76
- console.log();
77
-
78
- for (const site of sites) {
79
- const isDefault = config.defaultSite === site;
80
- const siteId = config.sites[site].siteId;
81
-
82
- if (isDefault) {
83
- console.log(` ${chalk.green("●")} ${site} ${chalk.dim("(default)")}${siteId ? ` ${chalk.dim(siteId)}` : ""}`);
84
- } else {
85
- console.log(` ${chalk.dim("○")} ${site}${siteId ? ` ${chalk.dim(siteId)}` : ""}`);
86
- }
87
- }
88
- console.log();
89
- });
63
+ .description("List all configured sites")
64
+ .action(async () => {
65
+ const config = await getConfig()
66
+ const sites = Object.keys(config.sites)
67
+
68
+ if (sites.length === 0) {
69
+ console.log(chalk.dim("No sites configured."))
70
+ console.log(
71
+ chalk.dim("Run `supalytics login` then `supalytics sites add <name>` to add a site.")
72
+ )
73
+ return
74
+ }
75
+
76
+ console.log()
77
+ console.log(chalk.bold("Configured Sites"))
78
+ console.log()
79
+
80
+ for (const site of sites) {
81
+ const isDefault = config.defaultSite === site
82
+ const siteId = config.sites[site].siteId
83
+
84
+ if (isDefault) {
85
+ console.log(
86
+ ` ${chalk.green("●")} ${site} ${chalk.dim("(default)")}${siteId ? ` ${chalk.dim(siteId)}` : ""}`
87
+ )
88
+ } else {
89
+ console.log(` ${chalk.dim("○")} ${site}${siteId ? ` ${chalk.dim(siteId)}` : ""}`)
90
+ }
91
+ }
92
+ console.log()
93
+ })
90
94
 
91
95
  // sites add <identifier>
92
96
  const addCommand = new Command("add")
93
- .description("Create a new site")
94
- .argument("<identifier>", "Domain or project name")
95
- .action(async (identifier: string) => {
96
- const auth = await getAuth();
97
- if (!auth) {
98
- console.error(chalk.red("Not logged in. Run `supalytics login` first."));
99
- process.exit(1);
100
- }
101
-
102
- console.log(chalk.dim("Creating site..."));
103
-
104
- try {
105
- const result = await createSiteViaApi(auth.accessToken, identifier);
106
-
107
- // Store locally (including website UUID for updates)
108
- await addSiteWithId(result.site.domain, result.site.site_id, result.site.id);
109
-
110
- console.log(chalk.green(`✓ Created ${result.site.domain}`));
111
- console.log(chalk.dim(` Site ID: ${result.site.site_id}`));
112
- console.log();
113
- console.log(chalk.dim("Add this to your HTML <head>:"));
114
- console.log();
115
- console.log(
116
- chalk.cyan(
117
- ` <script src="https://cdn.supalytics.co/script.js" data-site="${result.site.site_id}" defer></script>`
118
- )
119
- );
120
- console.log();
121
- } catch (error) {
122
- console.error(chalk.red(`Error: ${(error as Error).message}`));
123
- process.exit(1);
124
- }
125
- });
97
+ .description("Create a new site")
98
+ .argument("<identifier>", "Domain or project name")
99
+ .action(async (identifier: string) => {
100
+ const auth = await getAuth()
101
+ if (!auth) {
102
+ console.error(chalk.red("Not logged in. Run `supalytics login` first."))
103
+ process.exit(1)
104
+ }
105
+
106
+ console.log(chalk.dim("Creating site..."))
107
+
108
+ try {
109
+ const result = await createSiteViaApi(auth.accessToken, identifier)
110
+
111
+ // Store locally (including website UUID for updates)
112
+ await addSiteWithId(result.site.domain, result.site.site_id, result.site.id)
113
+
114
+ console.log(chalk.green(`✓ Created ${result.site.domain}`))
115
+ console.log(chalk.dim(` Site ID: ${result.site.site_id}`))
116
+ console.log()
117
+ console.log(chalk.dim("Add this to your HTML <head>:"))
118
+ console.log()
119
+ console.log(
120
+ chalk.cyan(
121
+ ` <script src="https://cdn.supalytics.co/script.js" data-site="${result.site.site_id}" defer></script>`
122
+ )
123
+ )
124
+ console.log()
125
+ } catch (error) {
126
+ console.error(chalk.red(`Error: ${(error as Error).message}`))
127
+ process.exit(1)
128
+ }
129
+ })
126
130
 
127
131
  // sites update <identifier> --domain <domain>
128
132
  const updateCommand = new Command("update")
129
- .description("Update a site's domain")
130
- .argument("<identifier>", "Current domain/identifier")
131
- .option("-d, --domain <domain>", "New domain name")
132
- .action(async (identifier: string, options: { domain?: string }) => {
133
- if (!options.domain) {
134
- console.error(chalk.red("Error: --domain is required"));
135
- console.error(chalk.dim("Usage: supalytics sites update <identifier> --domain <new-domain>"));
136
- process.exit(1);
137
- }
138
-
139
- const auth = await getAuth();
140
- if (!auth) {
141
- console.error(chalk.red("Not logged in. Run `supalytics login` first."));
142
- process.exit(1);
143
- }
144
-
145
- // Get site from local config
146
- const config = await getConfig();
147
- const siteConfig = config.sites[identifier];
148
- if (!siteConfig) {
149
- console.error(chalk.red(`Site '${identifier}' not found locally.`));
150
- const sites = Object.keys(config.sites);
151
- if (sites.length > 0) {
152
- console.log(chalk.dim(`Available sites: ${sites.join(", ")}`));
153
- }
154
- process.exit(1);
155
- }
156
-
157
- console.log(chalk.dim("Updating site..."));
158
-
159
- try {
160
- // Try to use locally stored website ID first
161
- let websiteId = siteConfig.id;
162
-
163
- // If no local ID, fetch from server
164
- if (!websiteId) {
165
- const listResponse = await fetch(`${WEB_BASE}/api/cli/sites`, {
166
- headers: { Authorization: `Bearer ${auth.accessToken}` },
167
- });
168
-
169
- if (!listResponse.ok) {
170
- throw new Error("Failed to fetch sites");
171
- }
172
-
173
- const { sites } = await listResponse.json();
174
- const site = sites.find((s: { domain: string }) => s.domain === identifier);
175
-
176
- if (!site) {
177
- throw new Error(`Site '${identifier}' not found on server`);
178
- }
179
- websiteId = site.id;
180
- }
181
-
182
- // Update via API
183
- const updateResponse = await fetch(`${WEB_BASE}/api/cli/sites/${websiteId}`, {
184
- method: "PATCH",
185
- headers: {
186
- Authorization: `Bearer ${auth.accessToken}`,
187
- "Content-Type": "application/json",
188
- },
189
- body: JSON.stringify({ domain: options.domain }),
190
- });
191
-
192
- if (!updateResponse.ok) {
193
- const error = await updateResponse.json();
194
- throw new Error(error.error || "Failed to update site");
195
- }
196
-
197
- const result: UpdateSiteResponse = await updateResponse.json();
198
-
199
- // Update local config (preserve all fields)
200
- const { siteId: existingSiteId, id: existingId } = siteConfig;
201
- const siteId = existingSiteId || result.site.site_id;
202
- const id = existingId || websiteId;
203
- delete config.sites[identifier];
204
- config.sites[options.domain] = { siteId, id };
205
- if (config.defaultSite === identifier) {
206
- config.defaultSite = options.domain;
207
- }
208
- await saveConfig(config);
209
-
210
- console.log(chalk.green(`✓ Updated ${identifier} → ${options.domain}`));
211
- } catch (error) {
212
- console.error(chalk.red(`Error: ${(error as Error).message}`));
213
- process.exit(1);
214
- }
215
- });
216
-
217
- sitesCommand.addCommand(addCommand);
218
- sitesCommand.addCommand(updateCommand);
133
+ .description("Update a site's domain")
134
+ .argument("<identifier>", "Current domain/identifier")
135
+ .option("-d, --domain <domain>", "New domain name")
136
+ .action(async (identifier: string, options: { domain?: string }) => {
137
+ if (!options.domain) {
138
+ console.error(chalk.red("Error: --domain is required"))
139
+ console.error(chalk.dim("Usage: supalytics sites update <identifier> --domain <new-domain>"))
140
+ process.exit(1)
141
+ }
142
+
143
+ const auth = await getAuth()
144
+ if (!auth) {
145
+ console.error(chalk.red("Not logged in. Run `supalytics login` first."))
146
+ process.exit(1)
147
+ }
148
+
149
+ // Get site from local config
150
+ const config = await getConfig()
151
+ const siteConfig = config.sites[identifier]
152
+ if (!siteConfig) {
153
+ console.error(chalk.red(`Site '${identifier}' not found locally.`))
154
+ const sites = Object.keys(config.sites)
155
+ if (sites.length > 0) {
156
+ console.log(chalk.dim(`Available sites: ${sites.join(", ")}`))
157
+ }
158
+ process.exit(1)
159
+ }
160
+
161
+ console.log(chalk.dim("Updating site..."))
162
+
163
+ try {
164
+ // Try to use locally stored website ID first
165
+ let websiteId = siteConfig.id
166
+
167
+ // If no local ID, fetch from server
168
+ if (!websiteId) {
169
+ const listResponse = await fetch(`${WEB_BASE}/api/cli/sites`, {
170
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
171
+ })
172
+
173
+ if (!listResponse.ok) {
174
+ throw new Error("Failed to fetch sites")
175
+ }
176
+
177
+ const { sites } = await listResponse.json()
178
+ const site = sites.find((s: { domain: string }) => s.domain === identifier)
179
+
180
+ if (!site) {
181
+ throw new Error(`Site '${identifier}' not found on server`)
182
+ }
183
+ websiteId = site.id
184
+ }
185
+
186
+ // Update via API
187
+ const updateResponse = await fetch(`${WEB_BASE}/api/cli/sites/${websiteId}`, {
188
+ method: "PATCH",
189
+ headers: {
190
+ Authorization: `Bearer ${auth.accessToken}`,
191
+ "Content-Type": "application/json",
192
+ },
193
+ body: JSON.stringify({ domain: options.domain }),
194
+ })
195
+
196
+ if (!updateResponse.ok) {
197
+ const error = await updateResponse.json()
198
+ throw new Error(error.error || "Failed to update site")
199
+ }
200
+
201
+ const result: UpdateSiteResponse = await updateResponse.json()
202
+
203
+ // Update local config (preserve all fields)
204
+ const { siteId: existingSiteId, id: existingId } = siteConfig
205
+ const siteId = existingSiteId || result.site.site_id
206
+ const id = existingId || websiteId
207
+ delete config.sites[identifier]
208
+ config.sites[options.domain] = { siteId, id }
209
+ if (config.defaultSite === identifier) {
210
+ config.defaultSite = options.domain
211
+ }
212
+ await saveConfig(config)
213
+
214
+ console.log(chalk.green(`✓ Updated ${identifier} → ${options.domain}`))
215
+ } catch (error) {
216
+ console.error(chalk.red(`Error: ${(error as Error).message}`))
217
+ process.exit(1)
218
+ }
219
+ })
220
+
221
+ sitesCommand.addCommand(addCommand)
222
+ sitesCommand.addCommand(updateCommand)
219
223
 
220
224
  export const defaultCommand = new Command("default")
221
- .description("Set the default site")
222
- .argument("<domain>", "Domain to set as default")
223
- .action(async (domain: string) => {
224
- const success = await setDefaultSite(domain);
225
-
226
- if (!success) {
227
- const config = await getConfig();
228
- const sites = Object.keys(config.sites);
229
- console.error(chalk.red(`Error: Site '${domain}' not found.`));
230
- if (sites.length > 0) {
231
- console.log(chalk.dim(`Available sites: ${sites.join(", ")}`));
232
- }
233
- process.exit(1);
234
- }
235
-
236
- console.log(chalk.green(`✓ Default site set to ${domain}`));
237
- });
225
+ .description("Set the default site")
226
+ .argument("<domain>", "Domain to set as default")
227
+ .action(async (domain: string) => {
228
+ const success = await setDefaultSite(domain)
229
+
230
+ if (!success) {
231
+ const config = await getConfig()
232
+ const sites = Object.keys(config.sites)
233
+ console.error(chalk.red(`Error: Site '${domain}' not found.`))
234
+ if (sites.length > 0) {
235
+ console.log(chalk.dim(`Available sites: ${sites.join(", ")}`))
236
+ }
237
+ process.exit(1)
238
+ }
239
+
240
+ console.log(chalk.green(`✓ Default site set to ${domain}`))
241
+ })
238
242
 
239
243
  export const removeCommand = new Command("remove")
240
- .description("Remove a site")
241
- .argument("<domain>", "Domain to remove")
242
- .action(async (domain: string) => {
243
- const success = await removeSite(domain);
244
-
245
- if (!success) {
246
- console.error(chalk.red(`Error: Site '${domain}' not found.`));
247
- process.exit(1);
248
- }
249
-
250
- console.log(chalk.green(`✓ Removed ${domain}`));
251
- });
244
+ .description("Remove a site from local CLI config (does not delete from server)")
245
+ .argument("<domain>", "Domain to remove")
246
+ .action(async (domain: string) => {
247
+ const success = await removeSite(domain)
248
+
249
+ if (!success) {
250
+ console.error(chalk.red(`Error: Site '${domain}' not found.`))
251
+ process.exit(1)
252
+ }
253
+
254
+ console.log(chalk.green(`✓ Removed ${domain}`))
255
+ })
@@ -6,8 +6,8 @@ import { logo, parsePeriod, truncate } from "../ui";
6
6
 
7
7
  export const statsCommand = new Command("stats")
8
8
  .description("Overview stats: pageviews, visitors, bounce rate, revenue")
9
- .argument("[period]", "Time period: today, yesterday, week, month, year, 7d, 30d, 90d, 12mo, all", "30d")
10
9
  .option("-s, --site <site>", "Site to query")
10
+ .option("-p, --period <period>", "Time period: today, yesterday, week, month, year, 7d, 30d, 90d, 12mo, all", "30d")
11
11
  .option("--start <date>", "Start date (YYYY-MM-DD)")
12
12
  .option("--end <date>", "End date (YYYY-MM-DD)")
13
13
  .option("-f, --filter <filters...>", "Filters in format 'field:operator:value'")
@@ -15,7 +15,7 @@ export const statsCommand = new Command("stats")
15
15
  .option("--no-revenue", "Exclude revenue metrics")
16
16
  .option("--json", "Output as JSON")
17
17
  .option("-t, --test", "Test mode: query localhost data instead of production")
18
- .action(async (period, options) => {
18
+ .action(async (options) => {
19
19
  const site = options.site || (await getDefaultSite());
20
20
 
21
21
  if (!site) {
@@ -25,7 +25,7 @@ export const statsCommand = new Command("stats")
25
25
 
26
26
  const dateRange = options.start && options.end
27
27
  ? [options.start, options.end] as [string, string]
28
- : parsePeriod(period);
28
+ : parsePeriod(options.period);
29
29
 
30
30
  const filters = options.filter
31
31
  ? options.filter.map((f: string) => {
@@ -65,7 +65,7 @@ export const statsCommand = new Command("stats")
65
65
  query(site, { metrics: ["visitors"], dimensions: ["country"], ...breakdownOpts }),
66
66
  query(site, { metrics: ["visitors"], dimensions: ["browser"], ...breakdownOpts }),
67
67
  query(site, { metrics: ["visitors"], dimensions: ["utm_source"], ...breakdownOpts }),
68
- listEvents(site, typeof dateRange === "string" ? dateRange : "30d", 10, options.test || false)
68
+ listEvents(site, dateRange, 10, options.test || false)
69
69
  );
70
70
  }
71
71
 
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { query, formatNumber } from "../api";
4
4
  import { getDefaultSite } from "../config";
5
- import { coloredSparkline } from "../ui";
5
+ import { coloredSparkline, parsePeriod } from "../ui";
6
6
 
7
7
  export const trendCommand = new Command("trend")
8
8
  .description("Daily visitor trend")
@@ -25,7 +25,7 @@ export const trendCommand = new Command("trend")
25
25
 
26
26
  const dateRange = options.start && options.end
27
27
  ? [options.start, options.end] as [string, string]
28
- : options.period;
28
+ : parsePeriod(options.period);
29
29
 
30
30
  // Parse filters
31
31
  const filters = options.filter