@supalytics/cli 0.3.4 → 0.3.6

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.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "CLI for Supalytics web analytics",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,10 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "dev": "bun run src/index.ts"
15
+ "dev": "bun run src/index.ts",
16
+ "release:patch": "npm version patch --no-git-tag-version && npm publish",
17
+ "release:minor": "npm version minor --no-git-tag-version && npm publish",
18
+ "release:major": "npm version major --no-git-tag-version && npm publish"
16
19
  },
17
20
  "keywords": [
18
21
  "analytics",
@@ -34,6 +37,7 @@
34
37
  "dependencies": {
35
38
  "chalk": "^5.6.2",
36
39
  "commander": "^14.0.2",
37
- "open": "^10.1.0"
40
+ "open": "^10.1.0",
41
+ "update-notifier": "^7.3.1"
38
42
  }
39
43
  }
package/src/api.ts CHANGED
@@ -327,3 +327,150 @@ export async function getRealtime(site: string, isDev: boolean = false): Promise
327
327
 
328
328
  return response.json();
329
329
  }
330
+
331
+ // Annotations API types
332
+ export interface AnnotationItem {
333
+ id: string;
334
+ date: string; // YYYY-MM-DD
335
+ title: string;
336
+ description?: string;
337
+ created_by: string;
338
+ created_at: string;
339
+ }
340
+
341
+ export interface AnnotationsResponse {
342
+ data: AnnotationItem[];
343
+ meta: {
344
+ domain: string;
345
+ date_range: [string | null, string | null];
346
+ };
347
+ }
348
+
349
+ /**
350
+ * List annotations for a site
351
+ */
352
+ export async function listAnnotations(
353
+ site: string,
354
+ period: string = "30d"
355
+ ): Promise<AnnotationsResponse> {
356
+ const apiKey = await getApiKeyForSite(site);
357
+
358
+ if (!apiKey) {
359
+ const sites = await getSites();
360
+ if (sites.length === 0) {
361
+ throw new Error(
362
+ "No sites configured. Run `supalytics login` to authenticate."
363
+ );
364
+ }
365
+ throw new Error(
366
+ `Not authenticated or site '${site}' not found. Available sites: ${sites.join(", ")}`
367
+ );
368
+ }
369
+
370
+ // Valid periods
371
+ const validPeriods = ["7d", "14d", "30d", "90d", "12mo", "all"] as const;
372
+ if (!validPeriods.includes(period as typeof validPeriods[number])) {
373
+ throw new Error(`Invalid period "${period}". Valid periods: ${validPeriods.join(", ")}`);
374
+ }
375
+
376
+ // Calculate date range from period
377
+ const endDate = new Date();
378
+ let startDate = new Date();
379
+
380
+ if (period === "7d") {
381
+ startDate.setDate(endDate.getDate() - 7);
382
+ } else if (period === "14d") {
383
+ startDate.setDate(endDate.getDate() - 14);
384
+ } else if (period === "30d") {
385
+ startDate.setDate(endDate.getDate() - 30);
386
+ } else if (period === "90d") {
387
+ startDate.setDate(endDate.getDate() - 90);
388
+ } else if (period === "12mo") {
389
+ startDate.setMonth(endDate.getMonth() - 12);
390
+ } else if (period === "all") {
391
+ startDate = new Date("2020-01-01");
392
+ }
393
+
394
+ const formatDate = (d: Date) => d.toISOString().split("T")[0];
395
+ const params = new URLSearchParams({
396
+ domain: site,
397
+ start_date: formatDate(startDate),
398
+ end_date: formatDate(endDate),
399
+ });
400
+
401
+ const response = await fetch(`${API_BASE}/v1/annotations?${params}`, {
402
+ headers: { Authorization: `Bearer ${apiKey}` },
403
+ });
404
+
405
+ if (!response.ok) {
406
+ const error = (await response.json()) as ApiError;
407
+ throw new Error(error.message || error.error || `HTTP ${response.status}`);
408
+ }
409
+
410
+ return response.json();
411
+ }
412
+
413
+ /**
414
+ * Create an annotation
415
+ */
416
+ export async function createAnnotation(
417
+ site: string,
418
+ date: string,
419
+ title: string,
420
+ description?: string
421
+ ): Promise<AnnotationItem> {
422
+ const apiKey = await getApiKeyForSite(site);
423
+
424
+ if (!apiKey) {
425
+ throw new Error(`Not authenticated. Run \`supalytics login\` first.`);
426
+ }
427
+
428
+ const response = await fetch(`${API_BASE}/v1/annotations`, {
429
+ method: "POST",
430
+ headers: {
431
+ Authorization: `Bearer ${apiKey}`,
432
+ "Content-Type": "application/json",
433
+ },
434
+ body: JSON.stringify({
435
+ domain: site,
436
+ date,
437
+ title,
438
+ description,
439
+ }),
440
+ });
441
+
442
+ if (!response.ok) {
443
+ const error = (await response.json()) as ApiError;
444
+ throw new Error(error.message || error.error || `HTTP ${response.status}`);
445
+ }
446
+
447
+ return response.json();
448
+ }
449
+
450
+ /**
451
+ * Delete an annotation
452
+ */
453
+ export async function deleteAnnotation(
454
+ site: string,
455
+ annotationId: string
456
+ ): Promise<void> {
457
+ const apiKey = await getApiKeyForSite(site);
458
+
459
+ if (!apiKey) {
460
+ throw new Error(`Not authenticated. Run \`supalytics login\` first.`);
461
+ }
462
+
463
+ const params = new URLSearchParams({ domain: site });
464
+ const response = await fetch(
465
+ `${API_BASE}/v1/annotations/${annotationId}?${params}`,
466
+ {
467
+ method: "DELETE",
468
+ headers: { Authorization: `Bearer ${apiKey}` },
469
+ }
470
+ );
471
+
472
+ if (!response.ok) {
473
+ const error = (await response.json()) as ApiError;
474
+ throw new Error(error.message || error.error || `HTTP ${response.status}`);
475
+ }
476
+ }
@@ -0,0 +1,139 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { listAnnotations, createAnnotation, deleteAnnotation } from "../api";
4
+ import { getDefaultSite } from "../config";
5
+
6
+ const annotationsDescription = `Manage chart annotations.
7
+
8
+ Examples:
9
+ # List all annotations
10
+ supalytics annotations
11
+
12
+ # List annotations for a specific period
13
+ supalytics annotations -p 90d
14
+
15
+ # Add an annotation
16
+ supalytics annotations add 2025-01-15 "Launched v2.0"
17
+
18
+ # Add an annotation with description
19
+ supalytics annotations add 2025-01-15 "Launched v2.0" -d "Major redesign with new dashboard"
20
+
21
+ # Remove an annotation
22
+ supalytics annotations remove <id>`;
23
+
24
+ export const annotationsCommand = new Command("annotations")
25
+ .description(annotationsDescription)
26
+ .argument("[period]", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
27
+ .option("-s, --site <site>", "Site to query")
28
+ .option("--json", "Output as JSON")
29
+ .action(async (period, options) => {
30
+ const site = options.site || (await getDefaultSite());
31
+
32
+ if (!site) {
33
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
34
+ process.exit(1);
35
+ }
36
+
37
+ try {
38
+ const response = await listAnnotations(site, period);
39
+
40
+ if (options.json) {
41
+ console.log(JSON.stringify(response, null, 2));
42
+ return;
43
+ }
44
+
45
+ const [startDate, endDate] = response.meta.date_range;
46
+ console.log();
47
+ console.log(chalk.bold("Annotations"), chalk.dim(`${startDate} → ${endDate}`));
48
+ console.log();
49
+
50
+ if (response.data.length === 0) {
51
+ console.log(chalk.dim(" No annotations found"));
52
+ console.log();
53
+ console.log(chalk.dim(" Add one with: supalytics annotations add <date> <title>"));
54
+ console.log();
55
+ return;
56
+ }
57
+
58
+ for (const annotation of response.data) {
59
+ console.log(` ${chalk.cyan(annotation.date)} ${annotation.title}`);
60
+ if (annotation.description) {
61
+ console.log(` ${chalk.dim(annotation.description)}`);
62
+ }
63
+ console.log(chalk.dim(` id: ${annotation.id}`));
64
+ }
65
+ console.log();
66
+ } catch (error) {
67
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
68
+ process.exit(1);
69
+ }
70
+ });
71
+
72
+ // Subcommand: add
73
+ annotationsCommand
74
+ .command("add <date> <title>")
75
+ .description("Add an annotation")
76
+ .option("-s, --site <site>", "Site to query")
77
+ .option("-d, --description <text>", "Optional description")
78
+ .option("--json", "Output as JSON")
79
+ .action(async (date, title, options) => {
80
+ const site = options.site || (await getDefaultSite());
81
+
82
+ if (!site) {
83
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
84
+ process.exit(1);
85
+ }
86
+
87
+ // Validate date format
88
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
89
+ console.error(chalk.red("Error: Date must be in YYYY-MM-DD format"));
90
+ process.exit(1);
91
+ }
92
+
93
+ try {
94
+ const annotation = await createAnnotation(site, date, title, options.description);
95
+
96
+ if (options.json) {
97
+ console.log(JSON.stringify(annotation, null, 2));
98
+ return;
99
+ }
100
+
101
+ console.log();
102
+ console.log(chalk.green("Annotation added"));
103
+ console.log();
104
+ console.log(` ${chalk.cyan(annotation.date)} ${annotation.title}`);
105
+ if (annotation.description) {
106
+ console.log(` ${chalk.dim(annotation.description)}`);
107
+ }
108
+ console.log(chalk.dim(` id: ${annotation.id}`));
109
+ console.log();
110
+ } catch (error) {
111
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ // Subcommand: remove
117
+ annotationsCommand
118
+ .command("remove <id>")
119
+ .description("Remove an annotation")
120
+ .option("-s, --site <site>", "Site to query")
121
+ .action(async (id, options) => {
122
+ const site = options.site || (await getDefaultSite());
123
+
124
+ if (!site) {
125
+ console.error(chalk.red("Error: No site specified. Use --site or set a default with `supalytics login --site`"));
126
+ process.exit(1);
127
+ }
128
+
129
+ try {
130
+ await deleteAnnotation(site, id);
131
+
132
+ console.log();
133
+ console.log(chalk.green("Annotation removed"));
134
+ console.log();
135
+ } catch (error) {
136
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
137
+ process.exit(1);
138
+ }
139
+ });
@@ -0,0 +1,54 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { $ } from "bun";
4
+
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
+ });
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { program } from "commander";
3
+ import updateNotifier from "update-notifier";
3
4
  import { loginCommand } from "./commands/login";
4
5
  import { logoutCommand } from "./commands/logout";
5
6
  import { sitesCommand, defaultCommand, removeCommand } from "./commands/sites";
@@ -12,7 +13,9 @@ import { queryCommand } from "./commands/query";
12
13
  import { eventsCommand } from "./commands/events";
13
14
  import { realtimeCommand } from "./commands/realtime";
14
15
  import { completionsCommand } from "./commands/completions";
16
+ import { annotationsCommand } from "./commands/annotations";
15
17
  import { initCommand } from "./commands/init";
18
+ import { updateCommand } from "./commands/update";
16
19
 
17
20
  const description = `CLI for Supalytics web analytics.
18
21
 
@@ -64,6 +67,9 @@ Output:
64
67
  // Read version from package.json
65
68
  const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
66
69
 
70
+ // Check for updates (runs in background, non-blocking)
71
+ updateNotifier({ pkg }).notify();
72
+
67
73
  program
68
74
  .name("supalytics")
69
75
  .description(description)
@@ -88,6 +94,10 @@ program.addCommand(trendCommand);
88
94
  program.addCommand(queryCommand);
89
95
  program.addCommand(eventsCommand);
90
96
  program.addCommand(realtimeCommand);
97
+ program.addCommand(annotationsCommand);
91
98
  program.addCommand(completionsCommand);
92
99
 
100
+ // Utility commands
101
+ program.addCommand(updateCommand);
102
+
93
103
  program.parse();