@supalytics/cli 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supalytics/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "CLI for Supalytics web analytics",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ })
package/src/index.ts CHANGED
@@ -1,105 +1,71 @@
1
1
  #!/usr/bin/env bun
2
- import { program } from "commander";
3
- import updateNotifier from "update-notifier";
4
- import { loginCommand } from "./commands/login";
5
- import { logoutCommand } from "./commands/logout";
6
- import { sitesCommand, defaultCommand, removeCommand } from "./commands/sites";
7
- import { statsCommand } from "./commands/stats";
8
- import { pagesCommand } from "./commands/pages";
9
- import { referrersCommand } from "./commands/referrers";
10
- import { countriesCommand } from "./commands/countries";
11
- import { trendCommand } from "./commands/trend";
12
- import { queryCommand } from "./commands/query";
13
- import { eventsCommand } from "./commands/events";
14
- import { realtimeCommand } from "./commands/realtime";
15
- import { completionsCommand } from "./commands/completions";
16
- import { annotationsCommand } from "./commands/annotations";
17
- import { journeysCommand } from "./commands/journeys";
18
- import { initCommand } from "./commands/init";
19
- import { updateCommand } from "./commands/update";
2
+ import { program } from "commander"
3
+ import updateNotifier from "update-notifier"
4
+ import { annotationsCommand } from "./commands/annotations"
5
+ import { completionsCommand } from "./commands/completions"
6
+ import { countriesCommand } from "./commands/countries"
7
+ import { eventsCommand } from "./commands/events"
8
+ import { initCommand } from "./commands/init"
9
+ import { journeysCommand } from "./commands/journeys"
10
+ import { loginCommand } from "./commands/login"
11
+ import { logoutCommand } from "./commands/logout"
12
+ import { pagesCommand } from "./commands/pages"
13
+ import { queryCommand } from "./commands/query"
14
+ import { realtimeCommand } from "./commands/realtime"
15
+ import { referrersCommand } from "./commands/referrers"
16
+ import { defaultCommand, removeCommand, sitesCommand } from "./commands/sites"
17
+ import { statsCommand } from "./commands/stats"
18
+ import { trendCommand } from "./commands/trend"
19
+ import { updateCommand } from "./commands/update"
20
20
 
21
21
  const description = `CLI for Supalytics web analytics.
22
22
 
23
- Quick Start:
24
- supalytics init Auto-setup: login → create site → get snippet
25
-
26
- Authentication:
27
- supalytics login Open browser to authenticate
28
- supalytics logout Log out and remove all credentials
29
-
30
- Site Management:
31
- supalytics sites List configured sites
32
- supalytics sites add <name> Create a new site
33
- supalytics sites update <name> --domain <domain> Update domain
34
- supalytics default <domain> Set default site
35
- supalytics remove <domain> Remove a site
36
-
37
23
  Date Ranges:
38
24
  --period: 7d, 14d, 30d, 90d, 12mo, all (default: 30d)
39
25
  --start/--end: Custom range in YYYY-MM-DD format
40
26
 
41
27
  Filters (-f, --filter):
42
28
  Format: field:operator:value
43
-
44
- Fields:
45
- page, country, region, city, browser, os, device,
46
- referrer, utm_source, utm_medium, utm_campaign, utm_content, utm_term,
47
- event, event_property, exit_link
48
-
49
- Operators:
50
- is Exact match (supports comma-separated: "country:is:US,UK")
51
- is_not Exclude exact match
52
- contains Substring match
53
- not_contains Exclude substring
54
- starts_with Prefix match
55
-
56
- Examples:
57
- -f "country:is:US"
58
- -f "page:contains:/blog"
59
- -f "device:is:mobile"
60
- -f "referrer:is:twitter.com"
61
- -f "event:is:signup"
62
- -f "event_property:is:plan:premium"
29
+ Fields: page, country, browser, device, referrer, utm_*, event
30
+ Operators: is, is_not, contains, not_contains, starts_with
31
+ Example: -f "country:is:US" -f "page:contains:/blog"
63
32
 
64
33
  Output:
65
- --json Raw JSON output (useful for piping to other tools or AI)
66
- --no-revenue Exclude revenue metrics from output`;
34
+ --json Raw JSON output
35
+ --no-revenue Exclude revenue metrics`
67
36
 
68
37
  // Read version from package.json
69
- const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
38
+ const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json()
70
39
 
71
40
  // Check for updates (runs in background, non-blocking)
72
- updateNotifier({ pkg }).notify();
41
+ updateNotifier({ pkg }).notify()
73
42
 
74
- program
75
- .name("supalytics")
76
- .description(description)
77
- .version(pkg.version);
43
+ program.name("supalytics").description(description).version(pkg.version)
78
44
 
79
45
  // Quick start
80
- program.addCommand(initCommand);
46
+ program.addCommand(initCommand)
81
47
 
82
48
  // Auth & site management commands
83
- program.addCommand(loginCommand);
84
- program.addCommand(logoutCommand);
85
- program.addCommand(sitesCommand);
86
- program.addCommand(defaultCommand);
87
- program.addCommand(removeCommand);
49
+ program.addCommand(loginCommand)
50
+ program.addCommand(logoutCommand)
51
+ program.addCommand(sitesCommand)
52
+ program.addCommand(defaultCommand)
53
+ program.addCommand(removeCommand)
88
54
 
89
55
  // Analytics commands
90
- program.addCommand(statsCommand);
91
- program.addCommand(pagesCommand);
92
- program.addCommand(referrersCommand);
93
- program.addCommand(countriesCommand);
94
- program.addCommand(trendCommand);
95
- program.addCommand(queryCommand);
96
- program.addCommand(eventsCommand);
97
- program.addCommand(journeysCommand);
98
- program.addCommand(realtimeCommand);
99
- program.addCommand(annotationsCommand);
100
- program.addCommand(completionsCommand);
56
+ program.addCommand(statsCommand)
57
+ program.addCommand(pagesCommand)
58
+ program.addCommand(referrersCommand)
59
+ program.addCommand(countriesCommand)
60
+ program.addCommand(trendCommand)
61
+ program.addCommand(queryCommand)
62
+ program.addCommand(eventsCommand)
63
+ program.addCommand(journeysCommand)
64
+ program.addCommand(realtimeCommand)
65
+ program.addCommand(annotationsCommand)
66
+ program.addCommand(completionsCommand)
101
67
 
102
68
  // Utility commands
103
- program.addCommand(updateCommand);
69
+ program.addCommand(updateCommand)
104
70
 
105
- program.parse();
71
+ program.parse()