@supalytics/cli 0.3.8 → 0.4.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.
@@ -1,15 +1,51 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import { CONFIG_FILE } from "../config";
4
- import { unlink } from "fs/promises";
1
+ import chalk from "chalk"
2
+ import { Command } from "commander"
3
+ import { clearAuth, getAuth } from "../config"
4
+
5
+ const WEB_BASE = process.env.SUPALYTICS_WEB_URL || "https://www.supalytics.co"
6
+
7
+ /**
8
+ * Revoke the PAT on the server
9
+ * Returns true if successful, false if failed (but we still proceed with local cleanup)
10
+ */
11
+ async function revokeToken(accessToken: string): Promise<boolean> {
12
+ try {
13
+ const response = await fetch(`${WEB_BASE}/api/cli/auth/revoke`, {
14
+ method: "DELETE",
15
+ headers: {
16
+ Authorization: `Bearer ${accessToken}`,
17
+ },
18
+ })
19
+
20
+ // Treat 200, 401, and 404 as success (token revoked or already invalid)
21
+ return response.ok || response.status === 401 || response.status === 404
22
+ } catch {
23
+ // Network errors shouldn't prevent local logout
24
+ return false
25
+ }
26
+ }
5
27
 
6
28
  export const logoutCommand = new Command("logout")
7
- .description("Log out and remove all stored credentials")
8
- .action(async () => {
9
- try {
10
- await unlink(CONFIG_FILE);
11
- console.log(chalk.green("✓ Logged out"));
12
- } catch {
13
- console.log(chalk.dim("Already logged out"));
14
- }
15
- });
29
+ .description("Log out and remove all stored credentials")
30
+ .action(async () => {
31
+ const auth = await getAuth()
32
+
33
+ if (!auth?.accessToken) {
34
+ console.log(chalk.dim("Already logged out"))
35
+ return
36
+ }
37
+
38
+ // Try to revoke token on server (don't block on failure)
39
+ const revoked = await revokeToken(auth.accessToken)
40
+
41
+ // Clear local config regardless of server result
42
+ await clearAuth()
43
+
44
+ if (revoked) {
45
+ console.log(chalk.green("Logged out"))
46
+ } else {
47
+ // Local logout succeeded but server revocation failed
48
+ console.log(chalk.green("Logged out"))
49
+ console.log(chalk.dim("Note: Could not reach server to revoke token"))
50
+ }
51
+ })
@@ -1,11 +1,10 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import { query, formatNumber, formatPercent, formatDuration } from "../api";
4
- import { getDefaultSite } from "../config";
5
- import { table, type TableColumn } from "../ui";
6
-
7
- const queryDescription = `Flexible query with custom metrics and dimensions.
1
+ import chalk from "chalk"
2
+ import { Command } from "commander"
3
+ import { formatDuration, formatNumber, formatPercent, query } from "../api"
4
+ import { getDefaultSite } from "../config"
5
+ import { type TableColumn, table } from "../ui"
8
6
 
7
+ const queryExamples = `
9
8
  Examples:
10
9
  # Top pages with revenue
11
10
  supalytics query -d page -m visitors,revenue
@@ -32,156 +31,180 @@ Examples:
32
31
  supalytics query -d country -f "event:is:signup"
33
32
 
34
33
  # Pages visited by premium users
35
- supalytics query -d page -f "event_property:is:plan:premium"`;
34
+ supalytics query -d page -f "event_property:is:plan:premium"`
36
35
 
37
36
  export const queryCommand = new Command("query")
38
- .description(queryDescription)
39
- .option("-s, --site <site>", "Site to query")
40
- .option("-m, --metrics <metrics>", "Metrics: visitors, bounce_rate, avg_session_duration, revenue, conversions", "visitors,revenue")
41
- .option("-d, --dimensions <dimensions>", "Dimensions: page, referrer, country, region, city, browser, os, device, date, hour, event, utm_* (max 2, event cannot combine)")
42
- .option("-f, --filter <filters...>", "Filters: field:operator:value (e.g., 'page:contains:/blog', 'event:is:signup', 'event_property:is:plan:premium')")
43
- .option("--sort <sort>", "Sort by field:order (e.g., 'revenue:desc', 'visitors:asc')")
44
- .option("--timezone <tz>", "Timezone for date grouping (e.g., 'America/New_York')", "UTC")
45
- .option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
46
- .option("--start <date>", "Start date (YYYY-MM-DD)")
47
- .option("--end <date>", "End date (YYYY-MM-DD)")
48
- .option("-l, --limit <number>", "Number of results (1-1000)", "10")
49
- .option("--offset <number>", "Skip results for pagination", "0")
50
- .option("--no-revenue", "Exclude revenue metrics")
51
- .option("--json", "Output as JSON")
52
- .option("-t, --test", "Test mode: query localhost data instead of production")
53
- .action(async (options) => {
54
- const site = options.site || (await getDefaultSite());
55
-
56
- if (!site) {
57
- console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
58
- process.exit(1);
59
- }
60
-
61
- const dateRange = options.start && options.end
62
- ? [options.start, options.end] as [string, string]
63
- : options.period;
64
-
65
- // Parse metrics
66
- const metrics = options.metrics.split(",").map((m: string) => m.trim());
67
-
68
- // Parse dimensions
69
- const dimensions = options.dimensions
70
- ? options.dimensions.split(",").map((d: string) => d.trim())
71
- : undefined;
72
-
73
- // Parse filters - NEW FORMAT: field:operator:value
74
- const filters = options.filter
75
- ? options.filter.map((f: string) => {
76
- const parts = f.split(":");
77
- if (parts.length < 3) {
78
- console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`));
79
- process.exit(1);
80
- }
81
- const [field, operator, ...valueParts] = parts;
82
- return [field, operator, valueParts.join(":")] as [string, string, string];
83
- })
84
- : undefined;
85
-
86
- // Parse sort
87
- const sort = options.sort
88
- ? (() => {
89
- const [field, order] = options.sort.split(":");
90
- if (!field || !order || (order !== "asc" && order !== "desc")) {
91
- console.error(chalk.red(`Invalid sort format: ${options.sort}. Use 'field:order' where order is 'asc' or 'desc'`));
92
- process.exit(1);
93
- }
94
- return { field, order: order as "asc" | "desc" };
95
- })()
96
- : undefined;
97
-
98
- try {
99
- const response = await query(site, {
100
- metrics,
101
- dimensions,
102
- filters,
103
- sort,
104
- timezone: options.timezone,
105
- date_range: dateRange,
106
- limit: parseInt(options.limit),
107
- offset: parseInt(options.offset),
108
- include_revenue: options.revenue !== false,
109
- is_dev: options.test || false,
110
- });
111
-
112
- if (options.json) {
113
- console.log(JSON.stringify(response, null, 2));
114
- return;
115
- }
116
-
117
- const [startDate, endDate] = response.meta.date_range;
118
-
119
- console.log();
120
- console.log(chalk.bold("Query Results"), chalk.dim(`${startDate} → ${endDate}`));
121
- console.log(chalk.dim(` ${response.data.length} rows in ${response.meta.query_ms}ms`));
122
- if (response.pagination.has_more) {
123
- console.log(chalk.dim(` More results available (offset: ${response.pagination.offset + response.pagination.limit})`));
124
- }
125
- console.log();
126
-
127
- if (response.data.length === 0) {
128
- console.log(chalk.dim(" No data"));
129
- return;
130
- }
131
-
132
- // Build columns from data
133
- const columns: TableColumn[] = [];
134
-
135
- // Add dimension columns
136
- if (response.data[0]?.dimensions) {
137
- for (const key of Object.keys(response.data[0].dimensions)) {
138
- columns.push({
139
- key: `dim_${key}`,
140
- label: key.toUpperCase(),
141
- align: "left",
142
- });
143
- }
144
- }
145
-
146
- // Add metric columns
147
- for (const key of Object.keys(response.data[0]?.metrics || {})) {
148
- columns.push({
149
- key: `met_${key}`,
150
- label: key.toUpperCase().replaceAll("_", " "),
151
- align: "right",
152
- format: (val) => {
153
- if (val === null || val === undefined) return "-";
154
- const v = val as number;
155
- if (key === "bounce_rate" || key === "conversion_rate") {
156
- return formatPercent(v);
157
- } else if (key === "avg_session_duration") {
158
- return formatDuration(v);
159
- } else if (key === "revenue") {
160
- return chalk.green("$" + (v / 100).toFixed(2));
161
- }
162
- return formatNumber(v);
163
- },
164
- });
165
- }
166
-
167
- // Transform data for table
168
- const tableData = response.data.map((r) => {
169
- const row: Record<string, unknown> = {};
170
- if (r.dimensions) {
171
- for (const [k, v] of Object.entries(r.dimensions)) {
172
- row[`dim_${k}`] = v || "(none)";
173
- }
174
- }
175
- for (const [k, v] of Object.entries(r.metrics)) {
176
- row[`met_${k}`] = v;
177
- }
178
- return row;
179
- });
180
-
181
- console.log(table(tableData, columns));
182
- console.log();
183
- } catch (error) {
184
- console.error(chalk.red(`Error: ${(error as Error).message}`));
185
- process.exit(1);
186
- }
187
- });
37
+ .description("Flexible analytics query with custom metrics and dimensions")
38
+ .addHelpText("after", queryExamples)
39
+ .option("-s, --site <site>", "Site to query")
40
+ .option(
41
+ "-m, --metrics <metrics>",
42
+ "Metrics: visitors, bounce_rate, avg_session_duration, revenue, conversions",
43
+ "visitors,revenue"
44
+ )
45
+ .option(
46
+ "-d, --dimensions <dimensions>",
47
+ "Dimensions: page, referrer, country, region, city, browser, os, device, date, hour, event, utm_* (max 2, event cannot combine)"
48
+ )
49
+ .option(
50
+ "-f, --filter <filters...>",
51
+ "Filters: field:operator:value (e.g., 'page:contains:/blog', 'event:is:signup', 'event_property:is:plan:premium')"
52
+ )
53
+ .option("--sort <sort>", "Sort by field:order (e.g., 'revenue:desc', 'visitors:asc')")
54
+ .option("--timezone <tz>", "Timezone for date grouping (e.g., 'America/New_York')", "UTC")
55
+ .option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
56
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
57
+ .option("--end <date>", "End date (YYYY-MM-DD)")
58
+ .option("-l, --limit <number>", "Number of results (1-1000)", "10")
59
+ .option("--offset <number>", "Skip results for pagination", "0")
60
+ .option("--no-revenue", "Exclude revenue metrics")
61
+ .option("--json", "Output as JSON")
62
+ .option("-t, --test", "Test mode: query localhost data instead of production")
63
+ .action(async (options) => {
64
+ const site = options.site || (await getDefaultSite())
65
+
66
+ if (!site) {
67
+ console.error(
68
+ chalk.red(
69
+ "Error: No site specified. Use --site or set a default with `supalytics login --site`"
70
+ )
71
+ )
72
+ process.exit(1)
73
+ }
74
+
75
+ const dateRange =
76
+ options.start && options.end
77
+ ? ([options.start, options.end] as [string, string])
78
+ : options.period
79
+
80
+ // Parse metrics
81
+ const metrics = options.metrics.split(",").map((m: string) => m.trim())
82
+
83
+ // Parse dimensions
84
+ const dimensions = options.dimensions
85
+ ? options.dimensions.split(",").map((d: string) => d.trim())
86
+ : undefined
87
+
88
+ // Parse filters - NEW FORMAT: field:operator:value
89
+ const filters = options.filter
90
+ ? options.filter.map((f: string) => {
91
+ const parts = f.split(":")
92
+ if (parts.length < 3) {
93
+ console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`))
94
+ process.exit(1)
95
+ }
96
+ const [field, operator, ...valueParts] = parts
97
+ return [field, operator, valueParts.join(":")] as [string, string, string]
98
+ })
99
+ : undefined
100
+
101
+ // Parse sort
102
+ const sort = options.sort
103
+ ? (() => {
104
+ const [field, order] = options.sort.split(":")
105
+ if (!field || !order || (order !== "asc" && order !== "desc")) {
106
+ console.error(
107
+ chalk.red(
108
+ `Invalid sort format: ${options.sort}. Use 'field:order' where order is 'asc' or 'desc'`
109
+ )
110
+ )
111
+ process.exit(1)
112
+ }
113
+ return { field, order: order as "asc" | "desc" }
114
+ })()
115
+ : undefined
116
+
117
+ try {
118
+ const response = await query(site, {
119
+ metrics,
120
+ dimensions,
121
+ filters,
122
+ sort,
123
+ timezone: options.timezone,
124
+ date_range: dateRange,
125
+ limit: parseInt(options.limit),
126
+ offset: parseInt(options.offset),
127
+ include_revenue: options.revenue !== false,
128
+ is_dev: options.test || false,
129
+ })
130
+
131
+ if (options.json) {
132
+ console.log(JSON.stringify(response, null, 2))
133
+ return
134
+ }
135
+
136
+ const [startDate, endDate] = response.meta.date_range
137
+
138
+ console.log()
139
+ console.log(chalk.bold("Query Results"), chalk.dim(`${startDate} → ${endDate}`))
140
+ console.log(chalk.dim(` ${response.data.length} rows in ${response.meta.query_ms}ms`))
141
+ if (response.pagination.has_more) {
142
+ console.log(
143
+ chalk.dim(
144
+ ` More results available (offset: ${response.pagination.offset + response.pagination.limit})`
145
+ )
146
+ )
147
+ }
148
+ console.log()
149
+
150
+ if (response.data.length === 0) {
151
+ console.log(chalk.dim(" No data"))
152
+ return
153
+ }
154
+
155
+ // Build columns from data
156
+ const columns: TableColumn[] = []
157
+
158
+ // Add dimension columns
159
+ if (response.data[0]?.dimensions) {
160
+ for (const key of Object.keys(response.data[0].dimensions)) {
161
+ columns.push({
162
+ key: `dim_${key}`,
163
+ label: key.toUpperCase(),
164
+ align: "left",
165
+ })
166
+ }
167
+ }
168
+
169
+ // Add metric columns
170
+ for (const key of Object.keys(response.data[0]?.metrics || {})) {
171
+ columns.push({
172
+ key: `met_${key}`,
173
+ label: key.toUpperCase().replaceAll("_", " "),
174
+ align: "right",
175
+ format: (val) => {
176
+ if (val === null || val === undefined) return "-"
177
+ const v = val as number
178
+ if (key === "bounce_rate" || key === "conversion_rate") {
179
+ return formatPercent(v)
180
+ } else if (key === "avg_session_duration") {
181
+ return formatDuration(v)
182
+ } else if (key === "revenue") {
183
+ return chalk.green("$" + (v / 100).toFixed(2))
184
+ }
185
+ return formatNumber(v)
186
+ },
187
+ })
188
+ }
189
+
190
+ // Transform data for table
191
+ const tableData = response.data.map((r) => {
192
+ const row: Record<string, unknown> = {}
193
+ if (r.dimensions) {
194
+ for (const [k, v] of Object.entries(r.dimensions)) {
195
+ row[`dim_${k}`] = v || "(none)"
196
+ }
197
+ }
198
+ for (const [k, v] of Object.entries(r.metrics)) {
199
+ row[`met_${k}`] = v
200
+ }
201
+ return row
202
+ })
203
+
204
+ console.log(table(tableData, columns))
205
+ console.log()
206
+ } catch (error) {
207
+ console.error(chalk.red(`Error: ${(error as Error).message}`))
208
+ process.exit(1)
209
+ }
210
+ })
@@ -1,54 +1,70 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import { $ } from "bun";
1
+ import { $ } from "bun"
2
+ import chalk from "chalk"
3
+ import { Command } from "commander"
4
4
 
5
5
  export const updateCommand = new Command("update")
6
- .description("Update Supalytics CLI to the latest version")
7
- .action(async () => {
8
- // Read current version
9
- const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json();
10
- console.log(chalk.dim(`Current version: ${pkg.version}`));
11
- console.log(chalk.dim("Checking for updates..."));
12
- console.log();
13
-
14
- try {
15
- // Check latest version from npm
16
- const response = await fetch("https://registry.npmjs.org/@supalytics/cli/latest");
17
- if (!response.ok) {
18
- throw new Error("Failed to check npm registry");
19
- }
20
- const data = await response.json();
21
- const latestVersion = data.version;
22
-
23
- if (latestVersion === pkg.version) {
24
- console.log(chalk.green("✓ Already on the latest version"));
25
- return;
26
- }
27
-
28
- console.log(chalk.cyan(`New version available: ${latestVersion}`));
29
- console.log();
30
- console.log(chalk.dim("Updating..."));
31
-
32
- // Try bun first, fall back to npm
33
- try {
34
- await $`bun upgrade @supalytics/cli`.quiet();
35
- console.log(chalk.green(`✓ Updated to ${latestVersion}`));
36
- } catch {
37
- // bun upgrade might not work for global packages, try npm
38
- try {
39
- await $`npm update -g @supalytics/cli`.quiet();
40
- console.log(chalk.green(`✓ Updated to ${latestVersion}`));
41
- } catch {
42
- console.log(chalk.yellow("Automatic update failed."));
43
- console.log();
44
- console.log("Please update manually:");
45
- console.log(chalk.cyan(" bun install -g @supalytics/cli@latest"));
46
- console.log(" or");
47
- console.log(chalk.cyan(" npm install -g @supalytics/cli@latest"));
48
- }
49
- }
50
- } catch (error) {
51
- console.error(chalk.red(`Error: ${(error as Error).message}`));
52
- process.exit(1);
53
- }
54
- });
6
+ .description("Update Supalytics CLI to the latest version")
7
+ .action(async () => {
8
+ // Read current version
9
+ const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json()
10
+ console.log(chalk.dim(`Current version: ${pkg.version}`))
11
+ console.log(chalk.dim("Checking for updates..."))
12
+ console.log()
13
+
14
+ try {
15
+ // Check latest version from npm
16
+ const response = await fetch("https://registry.npmjs.org/@supalytics/cli/latest")
17
+ if (!response.ok) {
18
+ throw new Error("Failed to check npm registry")
19
+ }
20
+ const data = await response.json()
21
+ const latestVersion = data.version
22
+
23
+ if (latestVersion === pkg.version) {
24
+ console.log(chalk.green("✓ Already on the latest version"))
25
+ return
26
+ }
27
+
28
+ console.log(chalk.cyan(`New version available: ${latestVersion}`))
29
+ console.log()
30
+ console.log(chalk.dim("Updating..."))
31
+
32
+ // Try to update using the appropriate package manager
33
+ let updated = false
34
+
35
+ // Try bun first (bun add -g for global packages)
36
+ try {
37
+ await $`bun add -g @supalytics/cli@latest`.quiet()
38
+ updated = true
39
+ } catch {
40
+ // bun not available or failed, try npm
41
+ }
42
+
43
+ // Fall back to npm if bun didn't work
44
+ if (!updated) {
45
+ try {
46
+ await $`npm install -g @supalytics/cli@latest`.quiet()
47
+ updated = true
48
+ } catch {
49
+ // npm also failed
50
+ }
51
+ }
52
+
53
+ if (!updated) {
54
+ console.log(chalk.yellow("Automatic update failed."))
55
+ console.log()
56
+ console.log("Please update manually:")
57
+ console.log(chalk.cyan(" bun add -g @supalytics/cli@latest"))
58
+ console.log(" or")
59
+ console.log(chalk.cyan(" npm install -g @supalytics/cli@latest"))
60
+ return
61
+ }
62
+
63
+ console.log(chalk.green(`✓ Updated to ${latestVersion}`))
64
+ console.log()
65
+ console.log(chalk.dim("Run `supalytics --version` to verify"))
66
+ } catch (error) {
67
+ console.error(chalk.red(`Error: ${(error as Error).message}`))
68
+ process.exit(1)
69
+ }
70
+ })