@supalytics/cli 0.3.5 → 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 +1 -1
- package/src/api.ts +147 -0
- package/src/commands/annotations.ts +139 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
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
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { queryCommand } from "./commands/query";
|
|
|
13
13
|
import { eventsCommand } from "./commands/events";
|
|
14
14
|
import { realtimeCommand } from "./commands/realtime";
|
|
15
15
|
import { completionsCommand } from "./commands/completions";
|
|
16
|
+
import { annotationsCommand } from "./commands/annotations";
|
|
16
17
|
import { initCommand } from "./commands/init";
|
|
17
18
|
import { updateCommand } from "./commands/update";
|
|
18
19
|
|
|
@@ -93,6 +94,7 @@ program.addCommand(trendCommand);
|
|
|
93
94
|
program.addCommand(queryCommand);
|
|
94
95
|
program.addCommand(eventsCommand);
|
|
95
96
|
program.addCommand(realtimeCommand);
|
|
97
|
+
program.addCommand(annotationsCommand);
|
|
96
98
|
program.addCommand(completionsCommand);
|
|
97
99
|
|
|
98
100
|
// Utility commands
|