@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.
- package/package.json +1 -1
- package/src/api.ts +259 -36
- package/src/commands/annotations.ts +26 -8
- package/src/commands/completions.ts +20 -9
- package/src/commands/countries.ts +2 -2
- package/src/commands/events.ts +14 -4
- package/src/commands/funnels.ts +485 -0
- package/src/commands/pages.ts +2 -2
- package/src/commands/query.ts +2 -2
- package/src/commands/referrers.ts +2 -2
- package/src/commands/sites.ts +231 -227
- package/src/commands/stats.ts +4 -4
- package/src/commands/trend.ts +2 -2
- package/src/index.ts +47 -79
- package/src/ui.ts +18 -2
package/src/commands/sites.ts
CHANGED
|
@@ -1,251 +1,255 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { Command } from "commander"
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
site: {
|
|
16
|
+
id: string
|
|
17
|
+
site_id: string
|
|
18
|
+
domain: string
|
|
19
|
+
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
interface UpdateSiteResponse {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
31
|
+
accessToken: string,
|
|
32
|
+
identifier: string
|
|
33
33
|
): Promise<CreateSiteResponse> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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/commands/stats.ts
CHANGED
|
@@ -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 (
|
|
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,
|
|
68
|
+
listEvents(site, dateRange, 10, options.test || false)
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
package/src/commands/trend.ts
CHANGED
|
@@ -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
|