@trillboards/cli 0.1.0

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.
Files changed (103) hide show
  1. package/README.md +1288 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/base-command.d.ts +20 -0
  7. package/dist/base-command.js +60 -0
  8. package/dist/commands/api.d.ts +14 -0
  9. package/dist/commands/api.js +80 -0
  10. package/dist/commands/audience/live.d.ts +14 -0
  11. package/dist/commands/audience/live.js +117 -0
  12. package/dist/commands/billing/usage.d.ts +10 -0
  13. package/dist/commands/billing/usage.js +92 -0
  14. package/dist/commands/billing.d.ts +6 -0
  15. package/dist/commands/billing.js +49 -0
  16. package/dist/commands/debug.d.ts +10 -0
  17. package/dist/commands/debug.js +156 -0
  18. package/dist/commands/doctor.d.ts +6 -0
  19. package/dist/commands/doctor.js +145 -0
  20. package/dist/commands/earnings.d.ts +15 -0
  21. package/dist/commands/earnings.js +182 -0
  22. package/dist/commands/endpoints/index.d.ts +9 -0
  23. package/dist/commands/endpoints/index.js +35 -0
  24. package/dist/commands/errors/index.d.ts +9 -0
  25. package/dist/commands/errors/index.js +34 -0
  26. package/dist/commands/fill-rate.d.ts +12 -0
  27. package/dist/commands/fill-rate.js +141 -0
  28. package/dist/commands/generated/generated-changelog-query.d.ts +14 -0
  29. package/dist/commands/generated/generated-changelog-query.js +35 -0
  30. package/dist/commands/generated/generated-data-api-aggregate.d.ts +15 -0
  31. package/dist/commands/generated/generated-data-api-aggregate.js +38 -0
  32. package/dist/commands/generated/generated-data-api-list-screens.d.ts +16 -0
  33. package/dist/commands/generated/generated-data-api-list-screens.js +41 -0
  34. package/dist/commands/generated/generated-data-api-lookalike-screens.d.ts +15 -0
  35. package/dist/commands/generated/generated-data-api-lookalike-screens.js +38 -0
  36. package/dist/commands/generated/generated-data-api-query-dsl.d.ts +13 -0
  37. package/dist/commands/generated/generated-data-api-query-dsl.js +32 -0
  38. package/dist/commands/generated/generated-data-api-screen-audience-history.d.ts +17 -0
  39. package/dist/commands/generated/generated-data-api-screen-audience-history.js +41 -0
  40. package/dist/commands/generated/generated-data-api-screen-audience-live.d.ts +14 -0
  41. package/dist/commands/generated/generated-data-api-screen-audience-live.js +32 -0
  42. package/dist/commands/generated/generated-data-api-semantic-search.d.ts +16 -0
  43. package/dist/commands/generated/generated-data-api-semantic-search.js +41 -0
  44. package/dist/commands/generated/generated-data-api-stats.d.ts +12 -0
  45. package/dist/commands/generated/generated-data-api-stats.js +26 -0
  46. package/dist/commands/generated/generated-data-api-stream.d.ts +14 -0
  47. package/dist/commands/generated/generated-data-api-stream.js +35 -0
  48. package/dist/commands/generated/generated-data-api-usage.d.ts +13 -0
  49. package/dist/commands/generated/generated-data-api-usage.js +32 -0
  50. package/dist/commands/generated/generated-device-ota-manifest.d.ts +13 -0
  51. package/dist/commands/generated/generated-device-ota-manifest.js +32 -0
  52. package/dist/commands/generated/generated-example-healthcheck.d.ts +13 -0
  53. package/dist/commands/generated/generated-example-healthcheck.js +32 -0
  54. package/dist/commands/generated/generated-playground-dry-run.d.ts +14 -0
  55. package/dist/commands/generated/generated-playground-dry-run.js +32 -0
  56. package/dist/commands/generated/generated-request-metadata-get.d.ts +14 -0
  57. package/dist/commands/generated/generated-request-metadata-get.js +32 -0
  58. package/dist/commands/generated/generated-sdk-enrich.d.ts +13 -0
  59. package/dist/commands/generated/generated-sdk-enrich.js +32 -0
  60. package/dist/commands/generated/generated-sdk-impression.d.ts +13 -0
  61. package/dist/commands/generated/generated-sdk-impression.js +32 -0
  62. package/dist/commands/generated/generated-sdk-qr-beacon.d.ts +13 -0
  63. package/dist/commands/generated/generated-sdk-qr-beacon.js +32 -0
  64. package/dist/commands/generated/generated-sdk-qr-scan.d.ts +13 -0
  65. package/dist/commands/generated/generated-sdk-qr-scan.js +32 -0
  66. package/dist/commands/generated/generated-sensing-paired-training-upload.d.ts +13 -0
  67. package/dist/commands/generated/generated-sensing-paired-training-upload.js +32 -0
  68. package/dist/commands/listen.d.ts +39 -0
  69. package/dist/commands/listen.js +245 -0
  70. package/dist/commands/login.d.ts +7 -0
  71. package/dist/commands/login.js +68 -0
  72. package/dist/commands/logs/tail.d.ts +47 -0
  73. package/dist/commands/logs/tail.js +240 -0
  74. package/dist/commands/reconcile.d.ts +9 -0
  75. package/dist/commands/reconcile.js +58 -0
  76. package/dist/commands/sandbox/create.d.ts +6 -0
  77. package/dist/commands/sandbox/create.js +35 -0
  78. package/dist/commands/sandbox/emit.d.ts +12 -0
  79. package/dist/commands/sandbox/emit.js +57 -0
  80. package/dist/commands/sandbox/reset.d.ts +6 -0
  81. package/dist/commands/sandbox/reset.js +34 -0
  82. package/dist/commands/sandbox/status.d.ts +6 -0
  83. package/dist/commands/sandbox/status.js +58 -0
  84. package/dist/commands/screens/watch.d.ts +14 -0
  85. package/dist/commands/screens/watch.js +158 -0
  86. package/dist/commands/trigger.d.ts +9 -0
  87. package/dist/commands/trigger.js +53 -0
  88. package/dist/commands/validate.d.ts +12 -0
  89. package/dist/commands/validate.js +93 -0
  90. package/dist/commands/whoami.d.ts +6 -0
  91. package/dist/commands/whoami.js +39 -0
  92. package/dist/config.d.ts +10 -0
  93. package/dist/config.js +32 -0
  94. package/dist/format-helpers.d.ts +46 -0
  95. package/dist/format-helpers.js +96 -0
  96. package/dist/http.d.ts +13 -0
  97. package/dist/http.js +51 -0
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.js +1 -0
  100. package/dist/output.d.ts +2 -0
  101. package/dist/output.js +78 -0
  102. package/oclif.manifest.json +2697 -0
  103. package/package.json +94 -0
@@ -0,0 +1,158 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { clearScreen, colorFillRate, colorStatus, colorize, formatCurrency, relativeTime, } from '../../format-helpers.js';
4
+ import { apiRequest } from '../../http.js';
5
+ export default class ScreensWatch extends BaseCommand {
6
+ static description = 'Live fleet dashboard — watch all screens in real-time';
7
+ static examples = [
8
+ '<%= config.bin %> screens watch',
9
+ '<%= config.bin %> screens watch --filter online',
10
+ '<%= config.bin %> screens watch --sort earnings',
11
+ '<%= config.bin %> screens watch --format json',
12
+ ];
13
+ static flags = {
14
+ filter: Flags.string({
15
+ description: 'Filter screens by status',
16
+ options: ['online', 'offline'],
17
+ }),
18
+ interval: Flags.integer({
19
+ default: 10,
20
+ description: 'Refresh interval in seconds',
21
+ }),
22
+ sort: Flags.string({
23
+ default: 'name',
24
+ description: 'Sort column',
25
+ options: ['name', 'earnings', 'fill-rate'],
26
+ }),
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(ScreensWatch);
30
+ this.setParsedFlags(flags);
31
+ const apiKey = this.getApiKey();
32
+ if (!apiKey) {
33
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
34
+ }
35
+ const baseUrl = this.getBaseUrl();
36
+ const format = this.getFormat();
37
+ // Single fetch for --format json (no live loop)
38
+ if (format === 'json') {
39
+ const data = await this.fetchData(apiKey, baseUrl);
40
+ const filtered = this.filterAndSort(data.screens, flags.filter, flags.sort ?? 'name');
41
+ this.output(filtered);
42
+ return;
43
+ }
44
+ // Live mode with setInterval
45
+ const render = async () => {
46
+ try {
47
+ const data = await this.fetchData(apiKey, baseUrl);
48
+ const screens = this.filterAndSort(data.screens, flags.filter, flags.sort ?? 'name');
49
+ clearScreen();
50
+ this.renderTable(screens, data.analytics);
51
+ }
52
+ catch (error) {
53
+ const msg = error instanceof Error ? error.message : String(error);
54
+ clearScreen();
55
+ this.log(colorize(`Error fetching fleet data: ${msg}`, 'red'));
56
+ this.log('Retrying...');
57
+ }
58
+ };
59
+ // Initial render
60
+ await render();
61
+ // Schedule refresh
62
+ const intervalMs = (flags.interval ?? 10) * 1000;
63
+ const timer = setInterval(() => {
64
+ render().catch(() => {
65
+ // Errors handled inside render
66
+ });
67
+ }, intervalMs);
68
+ // Clean exit on Ctrl+C
69
+ const cleanup = () => {
70
+ clearInterval(timer);
71
+ this.log('\nStopped watching.');
72
+ process.exit(0);
73
+ };
74
+ process.on('SIGINT', cleanup);
75
+ process.on('SIGTERM', cleanup);
76
+ // Keep process alive
77
+ await new Promise(() => {
78
+ // Never resolves — exits via signal handler
79
+ });
80
+ }
81
+ async fetchData(apiKey, baseUrl) {
82
+ const [healthResult, analyticsResult] = await Promise.allSettled([
83
+ apiRequest('GET', '/v1/partner/connect/health', { apiKey, baseUrl }),
84
+ apiRequest('GET', '/v1/partner/fleet/analytics', { apiKey, baseUrl }),
85
+ ]);
86
+ const health = healthResult.status === 'fulfilled' ? healthResult.value : { screens: [] };
87
+ const analytics = analyticsResult.status === 'fulfilled' ? analyticsResult.value : {};
88
+ return { analytics, screens: health.screens ?? [] };
89
+ }
90
+ filterAndSort(screens, filter, sort) {
91
+ let result = [...screens];
92
+ if (filter === 'online') {
93
+ result = result.filter((s) => s.status?.toLowerCase() === 'online');
94
+ }
95
+ else if (filter === 'offline') {
96
+ result = result.filter((s) => s.status?.toLowerCase() !== 'online');
97
+ }
98
+ switch (sort) {
99
+ case 'earnings': {
100
+ result.sort((a, b) => (b.earnings_today ?? 0) - (a.earnings_today ?? 0));
101
+ break;
102
+ }
103
+ case 'fill-rate': {
104
+ result.sort((a, b) => (b.fill_rate ?? 0) - (a.fill_rate ?? 0));
105
+ break;
106
+ }
107
+ default: {
108
+ result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+ renderTable(screens, analytics) {
114
+ const header = colorize('Trillboards Fleet Dashboard', 'bold');
115
+ const timestamp = new Date().toLocaleTimeString();
116
+ this.log(header);
117
+ this.log(`Updated: ${timestamp} | Online: ${analytics.screens_online ?? '?'}/${analytics.screens_total ?? '?'} | Network Fill: ${analytics.fill_rate != null ? colorFillRate(analytics.fill_rate) : 'N/A'}`);
118
+ this.log('');
119
+ // Column headers
120
+ const nameW = 30;
121
+ const statusW = 10;
122
+ const fillW = 12;
123
+ const earnW = 16;
124
+ const hbW = 18;
125
+ const headerRow = [
126
+ 'Name'.padEnd(nameW),
127
+ 'Status'.padEnd(statusW),
128
+ 'Fill Rate'.padEnd(fillW),
129
+ 'Earnings Today'.padEnd(earnW),
130
+ 'Last Heartbeat'.padEnd(hbW),
131
+ ].join(' ');
132
+ this.log(colorize(headerRow, 'dim'));
133
+ this.log('-'.repeat(nameW + statusW + fillW + earnW + hbW + 8));
134
+ if (screens.length === 0) {
135
+ this.log(' No screens found.');
136
+ return;
137
+ }
138
+ for (const screen of screens) {
139
+ const name = (screen.name ?? screen.screen_id ?? 'Unknown').slice(0, nameW).padEnd(nameW);
140
+ const statusRaw = screen.status ?? 'unknown';
141
+ let statusDisplay = statusRaw;
142
+ // Check for stale heartbeat (>60s)
143
+ if (statusRaw.toLowerCase() === 'online' && screen.last_heartbeat) {
144
+ const diffMs = Date.now() - new Date(screen.last_heartbeat).getTime();
145
+ if (diffMs > 60_000) {
146
+ statusDisplay = 'stale';
147
+ }
148
+ }
149
+ const status = colorStatus(statusDisplay).padEnd(statusW + 9); // +9 for ANSI codes
150
+ const fill = screen.fill_rate != null ? colorFillRate(screen.fill_rate).padEnd(fillW + 9) : 'N/A'.padEnd(fillW);
151
+ const earn = screen.earnings_today != null ? formatCurrency(screen.earnings_today).padEnd(earnW) : 'N/A'.padEnd(earnW);
152
+ const hb = screen.last_heartbeat ? relativeTime(screen.last_heartbeat).padEnd(hbW) : 'N/A'.padEnd(hbW);
153
+ this.log(`${name} ${status} ${fill} ${earn} ${hb}`);
154
+ }
155
+ this.log('');
156
+ this.log(colorize('Press Ctrl+C to stop', 'dim'));
157
+ }
158
+ }
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Trigger extends BaseCommand {
3
+ static args: {
4
+ event_type: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,53 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { apiRequest } from '../http.js';
4
+ const KNOWN_EVENT_TYPES = [
5
+ 'impression_start',
6
+ 'impression_complete',
7
+ 'impression_error',
8
+ 'screen_heartbeat',
9
+ 'screen_online',
10
+ 'screen_offline',
11
+ 'content_loaded',
12
+ 'content_error',
13
+ ];
14
+ export default class Trigger extends BaseCommand {
15
+ static args = {
16
+ event_type: Args.string({
17
+ description: 'Event type to fire (e.g. impression_start)',
18
+ required: false,
19
+ }),
20
+ };
21
+ static description = 'Fire test events in the sandbox';
22
+ static examples = [
23
+ '<%= config.bin %> trigger',
24
+ '<%= config.bin %> trigger impression_start',
25
+ '<%= config.bin %> trigger impression_complete --format json',
26
+ ];
27
+ async run() {
28
+ const { args, flags } = await this.parse(Trigger);
29
+ this.setParsedFlags(flags);
30
+ const apiKey = this.getApiKey();
31
+ if (!apiKey) {
32
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
33
+ }
34
+ const baseUrl = this.getBaseUrl();
35
+ if (!args.event_type) {
36
+ // List available event types
37
+ this.log('Available event types:');
38
+ this.output(KNOWN_EVENT_TYPES.map((type) => ({ event_type: type })));
39
+ return;
40
+ }
41
+ const eventType = args.event_type;
42
+ const result = await apiRequest('POST', '/v1/partner/sandbox/event', {
43
+ apiKey,
44
+ baseUrl,
45
+ body: {
46
+ event_type: eventType,
47
+ timestamp: new Date().toISOString(),
48
+ },
49
+ });
50
+ this.log(`Event fired: ${eventType}`);
51
+ this.output(result);
52
+ }
53
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Validate extends BaseCommand {
3
+ static args: {
4
+ file: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ operation: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,93 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../base-command.js';
4
+ import { apiRequest } from '../http.js';
5
+ export default class Validate extends BaseCommand {
6
+ static args = {
7
+ file: Args.string({ description: 'Path to JSON file to validate', required: true }),
8
+ };
9
+ static description = 'Validate a JSON payload against the API (dry-run)';
10
+ static examples = [
11
+ '<%= config.bin %> validate payload.json',
12
+ '<%= config.bin %> validate payload.json --operation create_screen',
13
+ ];
14
+ static flags = {
15
+ operation: Flags.string({
16
+ char: 'o',
17
+ description: 'Operation ID for the playground endpoint (e.g. create_screen)',
18
+ }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(Validate);
22
+ this.setParsedFlags(flags);
23
+ // Read and parse the file
24
+ let content;
25
+ try {
26
+ content = readFileSync(args.file, 'utf-8');
27
+ }
28
+ catch (error) {
29
+ const msg = error instanceof Error ? error.message : String(error);
30
+ this.error(`Could not read file: ${msg}`, { exit: 1 });
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(content);
35
+ }
36
+ catch (error) {
37
+ const msg = error instanceof Error ? error.message : String(error);
38
+ this.error(`Invalid JSON in ${args.file}: ${msg}`, { exit: 1 });
39
+ }
40
+ // If operation is specified and we have an API key, try the playground endpoint
41
+ const apiKey = this.getApiKey();
42
+ const baseUrl = this.getBaseUrl();
43
+ if (flags.operation && apiKey) {
44
+ try {
45
+ const result = await apiRequest('POST', `/v1/playground/${flags.operation}`, {
46
+ apiKey,
47
+ baseUrl,
48
+ body: parsed,
49
+ });
50
+ this.log('Validation passed (server-side):');
51
+ this.output(result);
52
+ return;
53
+ }
54
+ catch (error) {
55
+ // If the playground endpoint doesn't exist, fall through to local validation
56
+ if (error instanceof Error && 'statusCode' in error && error.statusCode === 404) {
57
+ this.warn('Playground endpoint not available. Showing local validation only.');
58
+ }
59
+ else {
60
+ throw error;
61
+ }
62
+ }
63
+ }
64
+ // Local validation: show the parsed structure
65
+ this.log('JSON is valid. Structure:');
66
+ const summary = summarizeJson(parsed);
67
+ this.output(summary);
68
+ }
69
+ }
70
+ function summarizeJson(data, depth = 0) {
71
+ if (depth > 3)
72
+ return { type: typeof data };
73
+ if (Array.isArray(data)) {
74
+ return {
75
+ items: data.length > 0 ? summarizeJson(data[0], depth + 1) : {},
76
+ length: data.length,
77
+ type: 'array',
78
+ };
79
+ }
80
+ if (typeof data === 'object' && data !== null) {
81
+ const fields = {};
82
+ for (const [key, value] of Object.entries(data)) {
83
+ if (typeof value === 'object' && value !== null) {
84
+ fields[key] = summarizeJson(value, depth + 1);
85
+ }
86
+ else {
87
+ fields[key] = typeof value;
88
+ }
89
+ }
90
+ return { fields, type: 'object' };
91
+ }
92
+ return { type: typeof data, value: data };
93
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Whoami extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,39 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ import { apiRequest } from '../http.js';
3
+ export default class Whoami extends BaseCommand {
4
+ static description = 'Show current partner info and API key status';
5
+ static examples = [
6
+ '<%= config.bin %> whoami',
7
+ '<%= config.bin %> whoami --format json',
8
+ ];
9
+ async run() {
10
+ const { flags } = await this.parse(Whoami);
11
+ this.setParsedFlags(flags);
12
+ const apiKey = this.getApiKey();
13
+ if (!apiKey) {
14
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
15
+ }
16
+ const baseUrl = this.getBaseUrl();
17
+ const info = (await apiRequest('GET', '/v1/partner/info', { apiKey, baseUrl }));
18
+ const maskedKey = maskApiKey(apiKey);
19
+ const format = this.getFormat();
20
+ if (format === 'json') {
21
+ this.output({ ...info, api_key: maskedKey });
22
+ return;
23
+ }
24
+ // Table/yaml — show a summary
25
+ const display = {
26
+ 'Partner Name': info.partner_name || info.name || 'Unknown',
27
+ Tier: info.tier || 'standard',
28
+ 'Screens Online': info.screens_online ?? 'N/A',
29
+ 'Screens Total': info.screens_total ?? 'N/A',
30
+ 'API Key': maskedKey,
31
+ };
32
+ this.output(display);
33
+ }
34
+ }
35
+ function maskApiKey(key) {
36
+ if (key.length <= 8)
37
+ return '****';
38
+ return key.slice(0, 4) + '...' + key.slice(-4);
39
+ }
@@ -0,0 +1,10 @@
1
+ export interface TrillboardsConfig {
2
+ api_key?: string;
3
+ base_url?: string;
4
+ default_format?: 'json' | 'table' | 'yaml';
5
+ partner_name?: string;
6
+ }
7
+ export declare function readConfig(): TrillboardsConfig;
8
+ export declare function writeConfig(config: TrillboardsConfig): void;
9
+ export declare function updateConfig(updates: Partial<TrillboardsConfig>): void;
10
+ export declare function getConfigPath(): string;
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const CONFIG_DIR = join(homedir(), '.trillboards');
5
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
6
+ function ensureConfigDir() {
7
+ if (!existsSync(CONFIG_DIR)) {
8
+ mkdirSync(CONFIG_DIR, { recursive: true });
9
+ }
10
+ }
11
+ export function readConfig() {
12
+ try {
13
+ if (!existsSync(CONFIG_FILE))
14
+ return {};
15
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ export function writeConfig(config) {
23
+ ensureConfigDir();
24
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
25
+ }
26
+ export function updateConfig(updates) {
27
+ const existing = readConfig();
28
+ writeConfig({ ...existing, ...updates });
29
+ }
30
+ export function getConfigPath() {
31
+ return CONFIG_FILE;
32
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared formatting helpers for DOOH CLI commands.
3
+ * Currency, percentages, counts, ANSI colors, ASCII bar charts.
4
+ */
5
+ export declare const ansi: {
6
+ readonly bold: "\u001B[1m";
7
+ readonly dim: "\u001B[2m";
8
+ readonly green: "\u001B[32m";
9
+ readonly red: "\u001B[31m";
10
+ readonly reset: "\u001B[0m";
11
+ readonly yellow: "\u001B[33m";
12
+ };
13
+ export declare function colorize(text: string, color: keyof typeof ansi): string;
14
+ /**
15
+ * Format a number as USD currency: $1,234.56
16
+ */
17
+ export declare function formatCurrency(value: number): string;
18
+ /**
19
+ * Format a number with comma separators: 1,234
20
+ */
21
+ export declare function formatCount(value: number): string;
22
+ /**
23
+ * Format a number as percentage: 78%
24
+ */
25
+ export declare function formatPercent(value: number): string;
26
+ /**
27
+ * Color a fill rate value: green >70%, yellow 50-70%, red <50%
28
+ */
29
+ export declare function colorFillRate(value: number): string;
30
+ /**
31
+ * Color a status string: green=online, red=offline, yellow=stale
32
+ */
33
+ export declare function colorStatus(status: string): string;
34
+ /**
35
+ * Render an ASCII bar chart row.
36
+ * maxWidth is the number of block characters at 100%.
37
+ */
38
+ export declare function barChartRow(label: string, value: number, maxValue: number, maxWidth?: number): string;
39
+ /**
40
+ * Format a relative time string from a timestamp.
41
+ */
42
+ export declare function relativeTime(timestamp: string | number | Date): string;
43
+ /**
44
+ * Clear the terminal screen and move cursor to top.
45
+ */
46
+ export declare function clearScreen(): void;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Shared formatting helpers for DOOH CLI commands.
3
+ * Currency, percentages, counts, ANSI colors, ASCII bar charts.
4
+ */
5
+ // ANSI color codes
6
+ export const ansi = {
7
+ bold: '\x1b[1m',
8
+ dim: '\x1b[2m',
9
+ green: '\x1b[32m',
10
+ red: '\x1b[31m',
11
+ reset: '\x1b[0m',
12
+ yellow: '\x1b[33m',
13
+ };
14
+ export function colorize(text, color) {
15
+ if (!process.stdout.isTTY)
16
+ return text;
17
+ return `${ansi[color]}${text}${ansi.reset}`;
18
+ }
19
+ /**
20
+ * Format a number as USD currency: $1,234.56
21
+ */
22
+ export function formatCurrency(value) {
23
+ return '$' + value.toLocaleString('en-US', { maximumFractionDigits: 2, minimumFractionDigits: 2 });
24
+ }
25
+ /**
26
+ * Format a number with comma separators: 1,234
27
+ */
28
+ export function formatCount(value) {
29
+ return value.toLocaleString('en-US');
30
+ }
31
+ /**
32
+ * Format a number as percentage: 78%
33
+ */
34
+ export function formatPercent(value) {
35
+ return Math.round(value) + '%';
36
+ }
37
+ /**
38
+ * Color a fill rate value: green >70%, yellow 50-70%, red <50%
39
+ */
40
+ export function colorFillRate(value) {
41
+ const text = formatPercent(value);
42
+ if (value >= 70)
43
+ return colorize(text, 'green');
44
+ if (value >= 50)
45
+ return colorize(text, 'yellow');
46
+ return colorize(text, 'red');
47
+ }
48
+ /**
49
+ * Color a status string: green=online, red=offline, yellow=stale
50
+ */
51
+ export function colorStatus(status) {
52
+ const lower = status.toLowerCase();
53
+ if (lower === 'online')
54
+ return colorize(status, 'green');
55
+ if (lower === 'offline')
56
+ return colorize(status, 'red');
57
+ return colorize(status, 'yellow');
58
+ }
59
+ /**
60
+ * Render an ASCII bar chart row.
61
+ * maxWidth is the number of block characters at 100%.
62
+ */
63
+ export function barChartRow(label, value, maxValue, maxWidth = 20) {
64
+ const ratio = maxValue > 0 ? value / maxValue : 0;
65
+ const filled = Math.round(ratio * maxWidth);
66
+ const empty = maxWidth - filled;
67
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
68
+ return `${label} ${bar} ${formatCurrency(value)}`;
69
+ }
70
+ /**
71
+ * Format a relative time string from a timestamp.
72
+ */
73
+ export function relativeTime(timestamp) {
74
+ const now = Date.now();
75
+ const then = new Date(timestamp).getTime();
76
+ const diffMs = now - then;
77
+ const diffSec = Math.floor(diffMs / 1000);
78
+ if (diffSec < 0)
79
+ return 'just now';
80
+ if (diffSec < 60)
81
+ return `${diffSec}s ago`;
82
+ const diffMin = Math.floor(diffSec / 60);
83
+ if (diffMin < 60)
84
+ return `${diffMin}m ago`;
85
+ const diffHr = Math.floor(diffMin / 60);
86
+ if (diffHr < 24)
87
+ return `${diffHr}h ago`;
88
+ const diffDay = Math.floor(diffHr / 24);
89
+ return `${diffDay}d ago`;
90
+ }
91
+ /**
92
+ * Clear the terminal screen and move cursor to top.
93
+ */
94
+ export function clearScreen() {
95
+ process.stdout.write('\x1b[2J\x1b[H');
96
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare class TrillboardsApiError extends Error {
2
+ code: string;
3
+ statusCode: number;
4
+ constructor(message: string, code: string, statusCode: number);
5
+ static fromResponse(body: Record<string, unknown>, statusCode: number): TrillboardsApiError;
6
+ }
7
+ export interface ApiRequestOptions {
8
+ apiKey: string;
9
+ baseUrl: string;
10
+ body?: unknown;
11
+ params?: Record<string, string>;
12
+ }
13
+ export declare function apiRequest(method: string, path: string, options: ApiRequestOptions): Promise<unknown>;
package/dist/http.js ADDED
@@ -0,0 +1,51 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ const { version: VERSION } = require('../package.json');
4
+ export class TrillboardsApiError extends Error {
5
+ code;
6
+ statusCode;
7
+ constructor(message, code, statusCode) {
8
+ super(message);
9
+ this.name = 'TrillboardsApiError';
10
+ this.code = code;
11
+ this.statusCode = statusCode;
12
+ }
13
+ static fromResponse(body, statusCode) {
14
+ const message = body.message || body.error || `API returned ${statusCode}`;
15
+ const code = body.code || `HTTP_${statusCode}`;
16
+ return new TrillboardsApiError(message, code, statusCode);
17
+ }
18
+ }
19
+ export async function apiRequest(method, path, options) {
20
+ const url = new URL(path, options.baseUrl);
21
+ if (options.params) {
22
+ for (const [k, v] of Object.entries(options.params)) {
23
+ url.searchParams.set(k, v);
24
+ }
25
+ }
26
+ const headers = {
27
+ 'Authorization': `Bearer ${options.apiKey}`,
28
+ 'Content-Type': 'application/json',
29
+ 'User-Agent': `trillboards-cli/${VERSION}`,
30
+ 'X-Trillboards-Client-Type': 'agent',
31
+ };
32
+ const response = await fetch(url.toString(), {
33
+ body: options.body ? JSON.stringify(options.body) : undefined,
34
+ headers,
35
+ method,
36
+ });
37
+ let json;
38
+ try {
39
+ json = (await response.json());
40
+ }
41
+ catch {
42
+ if (!response.ok) {
43
+ throw new TrillboardsApiError(`API returned ${response.status} with non-JSON body`, `HTTP_${response.status}`, response.status);
44
+ }
45
+ return { ok: true, status: response.status };
46
+ }
47
+ if (!response.ok) {
48
+ throw TrillboardsApiError.fromResponse(json, response.status);
49
+ }
50
+ return json;
51
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,2 @@
1
+ export type OutputFormat = 'json' | 'table' | 'yaml';
2
+ export declare function formatOutput(data: unknown, format: OutputFormat): string;