@supalytics/cli 0.3.2 → 0.3.4
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 +14 -14
- package/src/commands/init.ts +1 -1
- package/src/commands/login.ts +8 -14
- package/src/commands/sites.ts +6 -12
- package/src/config.ts +12 -10
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -61,11 +61,11 @@ export async function query(
|
|
|
61
61
|
const sites = await getSites();
|
|
62
62
|
if (sites.length === 0) {
|
|
63
63
|
throw new Error(
|
|
64
|
-
"No sites configured. Run `supalytics login
|
|
64
|
+
"No sites configured. Run `supalytics login` to authenticate."
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
throw new Error(
|
|
68
|
-
`
|
|
68
|
+
`Not authenticated or site '${site}' not found. Available sites: ${sites.join(", ")}`
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -75,7 +75,7 @@ export async function query(
|
|
|
75
75
|
Authorization: `Bearer ${apiKey}`,
|
|
76
76
|
"Content-Type": "application/json",
|
|
77
77
|
},
|
|
78
|
-
body: JSON.stringify(request),
|
|
78
|
+
body: JSON.stringify({ ...request, domain: site }),
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
if (!response.ok) {
|
|
@@ -169,15 +169,15 @@ export async function listEvents(
|
|
|
169
169
|
const sites = await getSites();
|
|
170
170
|
if (sites.length === 0) {
|
|
171
171
|
throw new Error(
|
|
172
|
-
"No sites configured. Run `supalytics login
|
|
172
|
+
"No sites configured. Run `supalytics login` to authenticate."
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
175
|
throw new Error(
|
|
176
|
-
`
|
|
176
|
+
`Not authenticated or site '${site}' not found. Available sites: ${sites.join(", ")}`
|
|
177
177
|
);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
const params = new URLSearchParams({ period, limit: String(limit) });
|
|
180
|
+
const params = new URLSearchParams({ period, limit: String(limit), domain: site });
|
|
181
181
|
if (isDev) {
|
|
182
182
|
params.set("is_dev", "true");
|
|
183
183
|
}
|
|
@@ -205,10 +205,10 @@ export async function getEventProperties(
|
|
|
205
205
|
const apiKey = await getApiKeyForSite(site);
|
|
206
206
|
|
|
207
207
|
if (!apiKey) {
|
|
208
|
-
throw new Error(`
|
|
208
|
+
throw new Error(`Not authenticated. Run \`supalytics login\` first.`);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
const params = new URLSearchParams({ period });
|
|
211
|
+
const params = new URLSearchParams({ period, domain: site });
|
|
212
212
|
if (isDev) {
|
|
213
213
|
params.set("is_dev", "true");
|
|
214
214
|
}
|
|
@@ -240,13 +240,14 @@ export async function getPropertyBreakdown(
|
|
|
240
240
|
const apiKey = await getApiKeyForSite(site);
|
|
241
241
|
|
|
242
242
|
if (!apiKey) {
|
|
243
|
-
throw new Error(`
|
|
243
|
+
throw new Error(`Not authenticated. Run \`supalytics login\` first.`);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
const params = new URLSearchParams({
|
|
247
247
|
period,
|
|
248
248
|
limit: String(limit),
|
|
249
249
|
include_revenue: String(includeRevenue),
|
|
250
|
+
domain: site,
|
|
250
251
|
});
|
|
251
252
|
if (isDev) {
|
|
252
253
|
params.set("is_dev", "true");
|
|
@@ -303,20 +304,19 @@ export async function getRealtime(site: string, isDev: boolean = false): Promise
|
|
|
303
304
|
const sites = await getSites();
|
|
304
305
|
if (sites.length === 0) {
|
|
305
306
|
throw new Error(
|
|
306
|
-
"No sites configured. Run `supalytics login
|
|
307
|
+
"No sites configured. Run `supalytics login` to authenticate."
|
|
307
308
|
);
|
|
308
309
|
}
|
|
309
310
|
throw new Error(
|
|
310
|
-
`
|
|
311
|
+
`Not authenticated or site '${site}' not found. Available sites: ${sites.join(", ")}`
|
|
311
312
|
);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
|
-
const params = new URLSearchParams();
|
|
315
|
+
const params = new URLSearchParams({ domain: site });
|
|
315
316
|
if (isDev) {
|
|
316
317
|
params.set("is_dev", "true");
|
|
317
318
|
}
|
|
318
|
-
const
|
|
319
|
-
const response = await fetch(url, {
|
|
319
|
+
const response = await fetch(`${API_BASE}/v1/realtime?${params}`, {
|
|
320
320
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
321
321
|
});
|
|
322
322
|
|
package/src/commands/init.ts
CHANGED
|
@@ -64,7 +64,7 @@ export const initCommand = new Command("init")
|
|
|
64
64
|
console.log(chalk.dim("Creating site..."));
|
|
65
65
|
|
|
66
66
|
const result = await createSiteViaApi(auth.accessToken, identifier);
|
|
67
|
-
await addSiteWithId(result.site.domain, result.
|
|
67
|
+
await addSiteWithId(result.site.domain, result.site.site_id, result.site.id);
|
|
68
68
|
|
|
69
69
|
console.log(chalk.green(`✓ Created ${result.site.domain}`));
|
|
70
70
|
|
package/src/commands/login.ts
CHANGED
|
@@ -19,7 +19,6 @@ interface DeviceCodeResponse {
|
|
|
19
19
|
interface TokenResponse {
|
|
20
20
|
access_token: string;
|
|
21
21
|
token_type: string;
|
|
22
|
-
expires_in: number;
|
|
23
22
|
user: {
|
|
24
23
|
id: string;
|
|
25
24
|
email: string;
|
|
@@ -100,10 +99,6 @@ interface SyncSite {
|
|
|
100
99
|
domain: string;
|
|
101
100
|
name: string | null;
|
|
102
101
|
};
|
|
103
|
-
apiKey: {
|
|
104
|
-
key: string;
|
|
105
|
-
prefix: string;
|
|
106
|
-
};
|
|
107
102
|
}
|
|
108
103
|
|
|
109
104
|
async function syncSites(accessToken: string): Promise<SyncSite[]> {
|
|
@@ -133,7 +128,7 @@ export async function loginWithDeviceFlow(forceResync = false): Promise<void> {
|
|
|
133
128
|
const existingAuth = await getAuth();
|
|
134
129
|
if (existingAuth && !forceResync) {
|
|
135
130
|
console.log(chalk.dim(`Already logged in as ${existingAuth.email}`));
|
|
136
|
-
console.log(chalk.dim(`Run 'supalytics login --resync' to refresh
|
|
131
|
+
console.log(chalk.dim(`Run 'supalytics login --resync' to refresh sites, or 'supalytics logout' to log out.`));
|
|
137
132
|
return;
|
|
138
133
|
}
|
|
139
134
|
|
|
@@ -143,15 +138,15 @@ export async function loginWithDeviceFlow(forceResync = false): Promise<void> {
|
|
|
143
138
|
try {
|
|
144
139
|
const sites = await syncSites(existingAuth.accessToken);
|
|
145
140
|
if (sites.length > 0) {
|
|
146
|
-
for (const { site
|
|
147
|
-
await addSiteWithId(site.domain,
|
|
141
|
+
for (const { site } of sites) {
|
|
142
|
+
await addSiteWithId(site.domain, site.site_id, site.id);
|
|
148
143
|
}
|
|
149
144
|
console.log(chalk.green(`✓ Synced ${sites.length} site${sites.length > 1 ? "s" : ""}`));
|
|
150
145
|
} else {
|
|
151
146
|
console.log(chalk.dim("No sites found."));
|
|
152
147
|
}
|
|
153
148
|
} catch {
|
|
154
|
-
console.log(chalk.yellow("Could not sync. Your
|
|
149
|
+
console.log(chalk.yellow("Could not sync. Your token may have been revoked. Run 'supalytics logout' then 'supalytics login'."));
|
|
155
150
|
}
|
|
156
151
|
return;
|
|
157
152
|
}
|
|
@@ -187,12 +182,11 @@ export async function loginWithDeviceFlow(forceResync = false): Promise<void> {
|
|
|
187
182
|
// 5. Ensure config directory exists
|
|
188
183
|
await mkdir(dirname(CONFIG_FILE), { recursive: true });
|
|
189
184
|
|
|
190
|
-
// 6. Save auth
|
|
185
|
+
// 6. Save auth (PAT doesn't expire by default)
|
|
191
186
|
await saveAuth({
|
|
192
187
|
accessToken: tokenResponse.access_token,
|
|
193
188
|
email: tokenResponse.user.email,
|
|
194
189
|
name: tokenResponse.user.name,
|
|
195
|
-
expiresAt: new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString(),
|
|
196
190
|
});
|
|
197
191
|
|
|
198
192
|
console.log(chalk.green(`✓ Logged in as ${tokenResponse.user.email}`));
|
|
@@ -202,8 +196,8 @@ export async function loginWithDeviceFlow(forceResync = false): Promise<void> {
|
|
|
202
196
|
try {
|
|
203
197
|
const sites = await syncSites(tokenResponse.access_token);
|
|
204
198
|
if (sites.length > 0) {
|
|
205
|
-
for (const { site
|
|
206
|
-
await addSiteWithId(site.domain,
|
|
199
|
+
for (const { site } of sites) {
|
|
200
|
+
await addSiteWithId(site.domain, site.site_id, site.id);
|
|
207
201
|
}
|
|
208
202
|
console.log(chalk.green(`✓ Synced ${sites.length} site${sites.length > 1 ? "s" : ""}`));
|
|
209
203
|
} else {
|
|
@@ -216,7 +210,7 @@ export async function loginWithDeviceFlow(forceResync = false): Promise<void> {
|
|
|
216
210
|
|
|
217
211
|
export const loginCommand = new Command("login")
|
|
218
212
|
.description("Authenticate with Supalytics")
|
|
219
|
-
.option("--resync", "Refresh
|
|
213
|
+
.option("--resync", "Refresh the list of sites")
|
|
220
214
|
.action(async (options: { resync?: boolean }) => {
|
|
221
215
|
try {
|
|
222
216
|
await loginWithDeviceFlow(options.resync || false);
|
package/src/commands/sites.ts
CHANGED
|
@@ -17,10 +17,6 @@ interface CreateSiteResponse {
|
|
|
17
17
|
site_id: string;
|
|
18
18
|
domain: string;
|
|
19
19
|
};
|
|
20
|
-
apiKey: {
|
|
21
|
-
key: string;
|
|
22
|
-
prefix: string;
|
|
23
|
-
};
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
interface UpdateSiteResponse {
|
|
@@ -81,13 +77,12 @@ export const sitesCommand = new Command("sites")
|
|
|
81
77
|
|
|
82
78
|
for (const site of sites) {
|
|
83
79
|
const isDefault = config.defaultSite === site;
|
|
84
|
-
const
|
|
85
|
-
const maskedKey = apiKey.slice(0, 8) + "..." + apiKey.slice(-4);
|
|
80
|
+
const siteId = config.sites[site].siteId;
|
|
86
81
|
|
|
87
82
|
if (isDefault) {
|
|
88
|
-
console.log(` ${chalk.green("●")} ${site} ${chalk.dim("(default)")} ${chalk.dim(
|
|
83
|
+
console.log(` ${chalk.green("●")} ${site} ${chalk.dim("(default)")}${siteId ? ` ${chalk.dim(siteId)}` : ""}`);
|
|
89
84
|
} else {
|
|
90
|
-
console.log(` ${chalk.dim("○")} ${site} ${chalk.dim(
|
|
85
|
+
console.log(` ${chalk.dim("○")} ${site}${siteId ? ` ${chalk.dim(siteId)}` : ""}`);
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
88
|
console.log();
|
|
@@ -110,11 +105,10 @@ const addCommand = new Command("add")
|
|
|
110
105
|
const result = await createSiteViaApi(auth.accessToken, identifier);
|
|
111
106
|
|
|
112
107
|
// Store locally (including website UUID for updates)
|
|
113
|
-
await addSiteWithId(result.site.domain, result.
|
|
108
|
+
await addSiteWithId(result.site.domain, result.site.site_id, result.site.id);
|
|
114
109
|
|
|
115
110
|
console.log(chalk.green(`✓ Created ${result.site.domain}`));
|
|
116
111
|
console.log(chalk.dim(` Site ID: ${result.site.site_id}`));
|
|
117
|
-
console.log(chalk.dim(` API key stored locally`));
|
|
118
112
|
console.log();
|
|
119
113
|
console.log(chalk.dim("Add this to your HTML <head>:"));
|
|
120
114
|
console.log();
|
|
@@ -203,11 +197,11 @@ const updateCommand = new Command("update")
|
|
|
203
197
|
const result: UpdateSiteResponse = await updateResponse.json();
|
|
204
198
|
|
|
205
199
|
// Update local config (preserve all fields)
|
|
206
|
-
const {
|
|
200
|
+
const { siteId: existingSiteId, id: existingId } = siteConfig;
|
|
207
201
|
const siteId = existingSiteId || result.site.site_id;
|
|
208
202
|
const id = existingId || websiteId;
|
|
209
203
|
delete config.sites[identifier];
|
|
210
|
-
config.sites[options.domain] = {
|
|
204
|
+
config.sites[options.domain] = { siteId, id };
|
|
211
205
|
if (config.defaultSite === identifier) {
|
|
212
206
|
config.defaultSite = options.domain;
|
|
213
207
|
}
|
package/src/config.ts
CHANGED
|
@@ -5,22 +5,24 @@ const CONFIG_DIR = join(homedir(), ".supalytics");
|
|
|
5
5
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
6
|
|
|
7
7
|
interface SiteConfig {
|
|
8
|
-
apiKey: string;
|
|
9
8
|
siteId?: string; // The site_id for tracking snippet
|
|
10
9
|
id?: string; // The website UUID for API operations
|
|
10
|
+
// Legacy: per-site API key (deprecated, kept for backward compatibility)
|
|
11
|
+
apiKey?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface AuthConfig {
|
|
14
|
-
accessToken: string;
|
|
15
|
+
accessToken: string; // PAT token (sly_pat_xxx)
|
|
15
16
|
email: string;
|
|
16
17
|
name: string;
|
|
18
|
+
// Legacy: expiresAt (PATs don't expire by default)
|
|
17
19
|
expiresAt?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
interface Config {
|
|
21
|
-
// User authentication
|
|
23
|
+
// User authentication (PAT-based)
|
|
22
24
|
auth?: AuthConfig;
|
|
23
|
-
// Map of domain ->
|
|
25
|
+
// Map of domain -> site config
|
|
24
26
|
sites: Record<string, SiteConfig>;
|
|
25
27
|
// Default site domain
|
|
26
28
|
defaultSite?: string;
|
|
@@ -50,9 +52,9 @@ export async function saveConfig(config: Config): Promise<void> {
|
|
|
50
52
|
await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
export async function addSite(domain: string,
|
|
55
|
+
export async function addSite(domain: string, siteId?: string, id?: string): Promise<void> {
|
|
54
56
|
const config = await getConfig();
|
|
55
|
-
config.sites[domain] = {
|
|
57
|
+
config.sites[domain] = { siteId, id };
|
|
56
58
|
// Set as default if it's the first site
|
|
57
59
|
if (!config.defaultSite || Object.keys(config.sites).length === 1) {
|
|
58
60
|
config.defaultSite = domain;
|
|
@@ -85,13 +87,14 @@ export async function setDefaultSite(domain: string): Promise<boolean> {
|
|
|
85
87
|
return true;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
export async function getApiKeyForSite(
|
|
90
|
+
export async function getApiKeyForSite(_domain: string): Promise<string | undefined> {
|
|
89
91
|
// Check env var first
|
|
90
92
|
if (process.env.SUPALYTICS_API_KEY) {
|
|
91
93
|
return process.env.SUPALYTICS_API_KEY;
|
|
92
94
|
}
|
|
95
|
+
// Use the PAT from auth - it works for all sites
|
|
93
96
|
const config = await getConfig();
|
|
94
|
-
return config.
|
|
97
|
+
return config.auth?.accessToken;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
export async function getDefaultSite(): Promise<string | undefined> {
|
|
@@ -135,12 +138,11 @@ export async function isAuthenticated(): Promise<boolean> {
|
|
|
135
138
|
// Add site with siteId and optional website id
|
|
136
139
|
export async function addSiteWithId(
|
|
137
140
|
domain: string,
|
|
138
|
-
apiKey: string,
|
|
139
141
|
siteId: string,
|
|
140
142
|
id?: string
|
|
141
143
|
): Promise<void> {
|
|
142
144
|
const config = await getConfig();
|
|
143
|
-
config.sites[domain] = {
|
|
145
|
+
config.sites[domain] = { siteId, id };
|
|
144
146
|
// Set as default if it's the first site
|
|
145
147
|
if (!config.defaultSite || Object.keys(config.sites).length === 1) {
|
|
146
148
|
config.defaultSite = domain;
|