@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,145 @@
1
+ import { createRequire } from 'node:module';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { getConfigPath, readConfig } from '../config.js';
4
+ const require = createRequire(import.meta.url);
5
+ const { version: VERSION } = require('../../package.json');
6
+ export default class Doctor extends BaseCommand {
7
+ static description = 'Run system diagnostics and check API connectivity';
8
+ static examples = [
9
+ '<%= config.bin %> doctor',
10
+ ];
11
+ async run() {
12
+ const { flags } = await this.parse(Doctor);
13
+ this.setParsedFlags(flags);
14
+ const checks = [];
15
+ const format = this.getFormat();
16
+ if (format !== 'json') {
17
+ this.log(`trillboards-cli v${VERSION}\n`);
18
+ }
19
+ // Check 1: API key configured
20
+ const apiKey = this.getApiKey();
21
+ if (apiKey) {
22
+ checks.push({ name: 'API key configured', status: 'pass' });
23
+ }
24
+ else {
25
+ checks.push({
26
+ fix: 'Run: trillboards login',
27
+ name: 'API key configured',
28
+ status: 'fail',
29
+ });
30
+ }
31
+ // Check 2: Config file exists
32
+ const config = readConfig();
33
+ if (config.api_key) {
34
+ checks.push({ name: `Config file (${getConfigPath()})`, status: 'pass' });
35
+ }
36
+ else {
37
+ checks.push({
38
+ fix: 'Run: trillboards login',
39
+ name: `Config file (${getConfigPath()})`,
40
+ status: 'warn',
41
+ });
42
+ }
43
+ // Check 3: Network — health endpoint
44
+ const baseUrl = this.getBaseUrl();
45
+ try {
46
+ const resp = await fetch(`${baseUrl}/health/live`, {
47
+ signal: AbortSignal.timeout(5000),
48
+ });
49
+ if (resp.ok) {
50
+ checks.push({ name: `API reachable (${baseUrl})`, status: 'pass' });
51
+ }
52
+ else {
53
+ checks.push({
54
+ fix: `API returned ${resp.status}. Check base URL.`,
55
+ name: `API reachable (${baseUrl})`,
56
+ status: 'fail',
57
+ });
58
+ }
59
+ }
60
+ catch {
61
+ checks.push({
62
+ fix: 'Check network connectivity and --base-url.',
63
+ name: `API reachable (${baseUrl})`,
64
+ status: 'fail',
65
+ });
66
+ }
67
+ // Check 4: API key valid
68
+ if (apiKey) {
69
+ try {
70
+ const resp = await fetch(`${baseUrl}/v1/partner/ping/auth`, {
71
+ headers: { Authorization: `Bearer ${apiKey}` },
72
+ signal: AbortSignal.timeout(5000),
73
+ });
74
+ if (resp.ok) {
75
+ checks.push({ name: 'API key valid', status: 'pass' });
76
+ }
77
+ else if (resp.status === 401) {
78
+ checks.push({
79
+ fix: 'Run: trillboards login (key may be expired or revoked)',
80
+ name: 'API key valid',
81
+ status: 'fail',
82
+ });
83
+ }
84
+ else {
85
+ checks.push({
86
+ fix: `Auth check returned ${resp.status}`,
87
+ name: 'API key valid',
88
+ status: 'warn',
89
+ });
90
+ }
91
+ }
92
+ catch {
93
+ checks.push({
94
+ fix: 'Could not reach auth endpoint.',
95
+ name: 'API key valid',
96
+ status: 'warn',
97
+ });
98
+ }
99
+ }
100
+ // Check 5: API discover endpoint
101
+ try {
102
+ const resp = await fetch(`${baseUrl}/api/discover`, {
103
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
104
+ signal: AbortSignal.timeout(5000),
105
+ });
106
+ if (resp.ok) {
107
+ checks.push({ name: 'API discovery endpoint', status: 'pass' });
108
+ }
109
+ else {
110
+ checks.push({
111
+ fix: `Discovery returned ${resp.status}.`,
112
+ name: 'API discovery endpoint',
113
+ status: 'warn',
114
+ });
115
+ }
116
+ }
117
+ catch {
118
+ checks.push({
119
+ fix: 'Could not reach discovery endpoint.',
120
+ name: 'API discovery endpoint',
121
+ status: 'warn',
122
+ });
123
+ }
124
+ // Render results
125
+ if (format === 'json') {
126
+ this.output(checks);
127
+ return;
128
+ }
129
+ for (const check of checks) {
130
+ const icon = check.status === 'pass' ? '\u2714' : check.status === 'fail' ? '\u2718' : '!';
131
+ const color = check.status === 'pass' ? '\u001B[32m' : check.status === 'fail' ? '\u001B[31m' : '\u001B[33m';
132
+ this.log(`${color}${icon}\u001B[0m ${check.name}`);
133
+ if (check.fix) {
134
+ this.log(` ${check.fix}`);
135
+ }
136
+ }
137
+ const failures = checks.filter((c) => c.status === 'fail');
138
+ if (failures.length > 0) {
139
+ this.log(`\n${failures.length} check(s) failed.`);
140
+ }
141
+ else {
142
+ this.log('\nAll checks passed.');
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Earnings extends BaseCommand {
3
+ static args: {
4
+ mode: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ period: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ private renderPeriodSummary;
13
+ private renderTodaySummary;
14
+ private runLive;
15
+ }
@@ -0,0 +1,182 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { barChartRow, colorize, formatCount, formatCurrency, formatPercent, } from '../format-helpers.js';
4
+ import { apiRequest } from '../http.js';
5
+ export default class Earnings extends BaseCommand {
6
+ static args = {
7
+ mode: Args.string({
8
+ description: 'Mode: "live" for real-time streaming',
9
+ options: ['live'],
10
+ required: false,
11
+ }),
12
+ };
13
+ static description = 'Revenue dashboard — today\'s summary, historical charts, or live streaming';
14
+ static examples = [
15
+ '<%= config.bin %> earnings',
16
+ '<%= config.bin %> earnings --period 30d',
17
+ '<%= config.bin %> earnings live',
18
+ '<%= config.bin %> earnings --format json',
19
+ ];
20
+ static flags = {
21
+ period: Flags.string({
22
+ default: '1d',
23
+ description: 'Time period (e.g. 1d, 7d, 30d)',
24
+ }),
25
+ };
26
+ async run() {
27
+ const { args, flags } = await this.parse(Earnings);
28
+ this.setParsedFlags(flags);
29
+ const apiKey = this.getApiKey();
30
+ if (!apiKey) {
31
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
32
+ }
33
+ const baseUrl = this.getBaseUrl();
34
+ const format = this.getFormat();
35
+ if (args.mode === 'live') {
36
+ await this.runLive(apiKey, baseUrl, format);
37
+ return;
38
+ }
39
+ const period = flags.period ?? '1d';
40
+ const data = await apiRequest('GET', '/v1/partner/connect/dashboard', {
41
+ apiKey,
42
+ baseUrl,
43
+ params: { period },
44
+ });
45
+ if (format === 'json') {
46
+ this.output(data);
47
+ return;
48
+ }
49
+ if (period === '1d') {
50
+ this.renderTodaySummary(data);
51
+ }
52
+ else {
53
+ this.renderPeriodSummary(data, period);
54
+ }
55
+ }
56
+ renderPeriodSummary(data, period) {
57
+ const totalRevenue = data.revenue ?? data.earnings ?? 0;
58
+ const dailyAvg = data.daily_average ?? 0;
59
+ this.log(colorize(`${period} Earnings: ${formatCurrency(totalRevenue)}`, 'bold'));
60
+ this.log(`Daily Average: ${formatCurrency(dailyAvg)}`);
61
+ this.log('');
62
+ const breakdown = data.daily_breakdown;
63
+ if (breakdown && breakdown.length > 0) {
64
+ const maxRevenue = Math.max(...breakdown.map((d) => d.revenue));
65
+ for (const day of breakdown) {
66
+ const dateLabel = formatDateLabel(day.date);
67
+ this.log(barChartRow(dateLabel, day.revenue, maxRevenue, 20));
68
+ }
69
+ }
70
+ else {
71
+ this.log(colorize('No daily breakdown available for this period.', 'dim'));
72
+ }
73
+ }
74
+ renderTodaySummary(data) {
75
+ const revenue = data.revenue ?? data.earnings ?? 0;
76
+ const impressions = data.impressions ?? 0;
77
+ const online = data.screens_online ?? 0;
78
+ const total = data.screens_total ?? 0;
79
+ const dailyAvg = data.daily_average ?? 0;
80
+ const trend = data.trend_percent ?? 0;
81
+ this.log(colorize(`Today's Earnings: ${formatCurrency(revenue)}`, 'bold'));
82
+ this.log(`Impressions: ${formatCount(impressions)}`);
83
+ this.log(`Screens Online: ${online}/${total}`);
84
+ this.log(`Daily Average: ${formatCurrency(dailyAvg)}`);
85
+ const trendStr = trend >= 0 ? `+${formatPercent(trend)}` : formatPercent(trend);
86
+ const trendColor = trend >= 0 ? 'green' : 'red';
87
+ this.log(`Trend: ${colorize(trendStr, trendColor)}`);
88
+ }
89
+ async runLive(apiKey, baseUrl, format) {
90
+ // Fetch initial dashboard data for the running total
91
+ let currentTotal = 0;
92
+ try {
93
+ const initial = await apiRequest('GET', '/v1/partner/connect/dashboard', {
94
+ apiKey,
95
+ baseUrl,
96
+ params: { period: '1d' },
97
+ });
98
+ currentTotal = initial.revenue ?? initial.earnings ?? 0;
99
+ }
100
+ catch {
101
+ // Start from 0 if initial fetch fails
102
+ }
103
+ this.log(colorize('Live Earnings Stream', 'bold'));
104
+ this.log('Connecting to event stream...');
105
+ this.log('');
106
+ const streamUrl = new URL('/v1/partner/events/stream', baseUrl);
107
+ streamUrl.searchParams.set('events', 'impression.recorded');
108
+ const headers = {
109
+ Accept: 'text/event-stream',
110
+ Authorization: `Bearer ${apiKey}`,
111
+ };
112
+ let response;
113
+ try {
114
+ response = await fetch(streamUrl.toString(), { headers });
115
+ }
116
+ catch (error) {
117
+ const msg = error instanceof Error ? error.message : String(error);
118
+ this.error(`Failed to connect to event stream: ${msg}`, { exit: 1 });
119
+ }
120
+ if (!response.ok) {
121
+ this.error(`Event stream returned ${response.status}`, { exit: 1 });
122
+ }
123
+ const reader = response.body?.getReader();
124
+ if (!reader) {
125
+ this.error('No readable stream from event endpoint', { exit: 1 });
126
+ }
127
+ const decoder = new TextDecoder();
128
+ const cleanup = () => {
129
+ reader.cancel().catch(() => {
130
+ // Ignore cancel errors during shutdown
131
+ });
132
+ this.log('\nStopped streaming.');
133
+ process.exit(0);
134
+ };
135
+ process.on('SIGINT', cleanup);
136
+ process.on('SIGTERM', cleanup);
137
+ // Read SSE stream
138
+ let buffer = '';
139
+ while (true) {
140
+ // eslint-disable-next-line no-await-in-loop
141
+ const { done, value } = await reader.read();
142
+ if (done)
143
+ break;
144
+ buffer += decoder.decode(value, { stream: true });
145
+ const lines = buffer.split('\n');
146
+ buffer = lines.pop() ?? '';
147
+ for (const line of lines) {
148
+ if (!line.startsWith('data:'))
149
+ continue;
150
+ const jsonStr = line.slice(5).trim();
151
+ if (!jsonStr)
152
+ continue;
153
+ try {
154
+ const event = JSON.parse(jsonStr);
155
+ const increment = event.revenue ?? event.cpm ?? 0;
156
+ currentTotal += increment;
157
+ if (format === 'json') {
158
+ this.log(JSON.stringify({ event, running_total: currentTotal }));
159
+ }
160
+ else {
161
+ const incStr = increment > 0 ? ` (+${formatCurrency(increment)})` : '';
162
+ process.stdout.write(`\rToday: ${formatCurrency(currentTotal)}${incStr} `);
163
+ }
164
+ }
165
+ catch {
166
+ // Skip malformed SSE data
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ function formatDateLabel(dateStr) {
173
+ try {
174
+ const d = new Date(dateStr);
175
+ const month = d.toLocaleString('en-US', { month: 'short' });
176
+ const day = String(d.getDate()).padStart(2, '0');
177
+ return `${month} ${day}`;
178
+ }
179
+ catch {
180
+ return dateStr.slice(0, 10);
181
+ }
182
+ }
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class Endpoints extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,35 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { apiRequest } from '../../http.js';
4
+ export default class Endpoints extends BaseCommand {
5
+ static description = 'Discover available API endpoints';
6
+ static examples = [
7
+ '<%= config.bin %> endpoints',
8
+ '<%= config.bin %> endpoints --search screens',
9
+ '<%= config.bin %> endpoints --format json',
10
+ ];
11
+ static flags = {
12
+ search: Flags.string({ char: 's', description: 'Search query to filter endpoints' }),
13
+ };
14
+ async run() {
15
+ const { flags } = await this.parse(Endpoints);
16
+ this.setParsedFlags(flags);
17
+ const apiKey = this.getApiKey();
18
+ if (!apiKey) {
19
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
20
+ }
21
+ const baseUrl = this.getBaseUrl();
22
+ if (flags.search) {
23
+ const result = await apiRequest('GET', '/api/discover/search', {
24
+ apiKey,
25
+ baseUrl,
26
+ params: { q: flags.search },
27
+ });
28
+ this.output(result);
29
+ }
30
+ else {
31
+ const result = await apiRequest('GET', '/api/discover', { apiKey, baseUrl });
32
+ this.output(result);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class Errors extends BaseCommand {
3
+ static args: {
4
+ code: 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,34 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { apiRequest } from '../../http.js';
4
+ export default class Errors extends BaseCommand {
5
+ static args = {
6
+ code: Args.string({ description: 'Error code to look up (e.g. screen_not_found)', required: false }),
7
+ };
8
+ static description = 'Look up error codes from the error catalog';
9
+ static examples = [
10
+ '<%= config.bin %> errors',
11
+ '<%= config.bin %> errors screen_not_found',
12
+ '<%= config.bin %> errors --format json',
13
+ ];
14
+ async run() {
15
+ const { args, flags } = await this.parse(Errors);
16
+ this.setParsedFlags(flags);
17
+ const apiKey = this.getApiKey();
18
+ if (!apiKey) {
19
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
20
+ }
21
+ const baseUrl = this.getBaseUrl();
22
+ if (args.code) {
23
+ // Single error lookup
24
+ const result = await apiRequest('GET', `/v1/errors/${args.code}`, { apiKey, baseUrl });
25
+ this.output(result);
26
+ }
27
+ else {
28
+ // List all errors
29
+ const result = (await apiRequest('GET', '/v1/errors', { apiKey, baseUrl }));
30
+ const errors = (result.errors || result.data || result);
31
+ this.output(errors);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class FillRate extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ by: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ private byHour;
10
+ private byScreen;
11
+ private overall;
12
+ }
@@ -0,0 +1,141 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { colorFillRate, colorize } from '../format-helpers.js';
4
+ import { apiRequest } from '../http.js';
5
+ export default class FillRate extends BaseCommand {
6
+ static description = 'Fill rate analysis — overall, per-screen, or by time';
7
+ static examples = [
8
+ '<%= config.bin %> fill-rate',
9
+ '<%= config.bin %> fill-rate --by screen',
10
+ '<%= config.bin %> fill-rate --by hour',
11
+ '<%= config.bin %> fill-rate --format json',
12
+ ];
13
+ static flags = {
14
+ by: Flags.string({
15
+ description: 'Breakdown dimension',
16
+ options: ['partner', 'screen', 'hour'],
17
+ }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(FillRate);
21
+ this.setParsedFlags(flags);
22
+ const apiKey = this.getApiKey();
23
+ if (!apiKey) {
24
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
25
+ }
26
+ const baseUrl = this.getBaseUrl();
27
+ const format = this.getFormat();
28
+ const by = flags.by;
29
+ if (by === 'screen') {
30
+ await this.byScreen(apiKey, baseUrl, format);
31
+ }
32
+ else if (by === 'hour') {
33
+ await this.byHour(apiKey, baseUrl, format);
34
+ }
35
+ else {
36
+ await this.overall(apiKey, baseUrl, format);
37
+ }
38
+ }
39
+ async byHour(apiKey, baseUrl, format) {
40
+ // Hourly breakdown isn't in a single endpoint — use fleet analytics daily breakdown as approximation
41
+ const data = await apiRequest('GET', '/v1/partner/fleet/analytics', {
42
+ apiKey,
43
+ baseUrl,
44
+ params: { period: '7d' },
45
+ });
46
+ if (format === 'json') {
47
+ this.output(data);
48
+ return;
49
+ }
50
+ this.log(colorize('Fill Rate by Day (7d)', 'bold'));
51
+ this.log(colorize('Note: Hourly breakdown requires per-screen analytics. Showing daily trend.', 'dim'));
52
+ this.log('');
53
+ const breakdown = data.daily_breakdown;
54
+ if (breakdown && breakdown.length > 0) {
55
+ const nameW = 12;
56
+ const fillW = 12;
57
+ const impW = 14;
58
+ const reqW = 14;
59
+ this.log([
60
+ 'Date'.padEnd(nameW),
61
+ 'Fill Rate'.padEnd(fillW),
62
+ 'Impressions'.padEnd(impW),
63
+ 'Requests'.padEnd(reqW),
64
+ ].join(' '));
65
+ this.log('-'.repeat(nameW + fillW + impW + reqW + 6));
66
+ for (const day of breakdown) {
67
+ const dateLabel = day.date.slice(0, 10).padEnd(nameW);
68
+ const fill = day.fill_rate != null ? colorFillRate(day.fill_rate).padEnd(fillW + 9) : 'N/A'.padEnd(fillW);
69
+ const imp = (day.impressions ?? 0).toLocaleString('en-US').padEnd(impW);
70
+ const req = (day.requests ?? 0).toLocaleString('en-US').padEnd(reqW);
71
+ this.log(`${dateLabel} ${fill} ${imp} ${req}`);
72
+ }
73
+ }
74
+ else {
75
+ this.log('No daily breakdown available.');
76
+ }
77
+ }
78
+ async byScreen(apiKey, baseUrl, format) {
79
+ const [healthResult, fleetResult] = await Promise.allSettled([
80
+ apiRequest('GET', '/v1/partner/connect/health', { apiKey, baseUrl }),
81
+ apiRequest('GET', '/v1/partner/fleet/analytics', { apiKey, baseUrl }),
82
+ ]);
83
+ const health = healthResult.status === 'fulfilled' ? healthResult.value : { screens: [] };
84
+ const fleet = fleetResult.status === 'fulfilled' ? fleetResult.value : {};
85
+ const screens = health.screens ?? [];
86
+ // Sort worst-first
87
+ const sorted = [...screens].sort((a, b) => (a.fill_rate ?? 0) - (b.fill_rate ?? 0));
88
+ if (format === 'json') {
89
+ this.output({ network_average: fleet.fill_rate, screens: sorted });
90
+ return;
91
+ }
92
+ this.log(colorize('Fill Rate by Screen (worst first)', 'bold'));
93
+ if (fleet.fill_rate != null) {
94
+ this.log(`Network average: ${colorFillRate(fleet.fill_rate)}`);
95
+ }
96
+ this.log('');
97
+ const nameW = 30;
98
+ const statusW = 10;
99
+ const fillW = 12;
100
+ this.log([
101
+ 'Screen'.padEnd(nameW),
102
+ 'Status'.padEnd(statusW),
103
+ 'Fill Rate'.padEnd(fillW),
104
+ ].join(' '));
105
+ this.log('-'.repeat(nameW + statusW + fillW + 4));
106
+ if (sorted.length === 0) {
107
+ this.log(' No screens found.');
108
+ return;
109
+ }
110
+ for (const screen of sorted) {
111
+ const name = (screen.name ?? screen.screen_id ?? 'Unknown').slice(0, nameW).padEnd(nameW);
112
+ const status = (screen.status ?? 'unknown').padEnd(statusW);
113
+ const fill = screen.fill_rate != null ? colorFillRate(screen.fill_rate) : 'N/A';
114
+ this.log(`${name} ${status} ${fill}`);
115
+ }
116
+ }
117
+ async overall(apiKey, baseUrl, format) {
118
+ const data = await apiRequest('GET', '/v1/partner/fleet/analytics', { apiKey, baseUrl });
119
+ if (format === 'json') {
120
+ this.output(data);
121
+ return;
122
+ }
123
+ this.log(colorize('Fill Rate Summary', 'bold'));
124
+ this.log('');
125
+ const fillRate = data.fill_rate ?? 0;
126
+ this.log(`Overall Fill Rate: ${colorFillRate(fillRate)}`);
127
+ this.log(`Total Impressions: ${(data.impressions ?? 0).toLocaleString('en-US')}`);
128
+ this.log(`Total Requests: ${(data.requests ?? 0).toLocaleString('en-US')}`);
129
+ this.log(`Screens Online: ${data.screens_online ?? 'N/A'}`);
130
+ this.log('');
131
+ if (fillRate < 50) {
132
+ this.log(colorize('Fill rate is below 50%. Run: trillboards fill-rate --by screen', 'yellow'));
133
+ }
134
+ else if (fillRate < 70) {
135
+ this.log(colorize('Fill rate is moderate. Check per-screen rates: trillboards fill-rate --by screen', 'yellow'));
136
+ }
137
+ else {
138
+ this.log(colorize('Fill rate is healthy.', 'green'));
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class Querychangelog extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {};
6
+ static flags: {
7
+ since: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,35 @@
1
+ // AUTO-GENERATED from OpenAPI spec (partner-api-generated) -- do not edit manually
2
+ // Regenerate with: npm run generate
3
+ import { Flags } from '@oclif/core';
4
+ import { BaseCommand } from '../../base-command.js';
5
+ import { apiRequest } from '../../http.js';
6
+ import { formatOutput } from '../../output.js';
7
+ export default class Querychangelog extends BaseCommand {
8
+ static description = "Query API changelog";
9
+ static examples = [`$ trillboards generated changelog query`];
10
+ static args = {};
11
+ static flags = {
12
+ ...BaseCommand.baseFlags,
13
+ 'since': Flags.string({ description: 'Only entries dated on or after this date (YYYY-MM-DD). Unreleased entries always included.' }),
14
+ 'type': Flags.string({ description: 'Filter by change category: "breaking" (changed+removed), "additive" (added), "deprecation" (deprecated), "fix" (fixed). Comma-separated for multiple.' }),
15
+ };
16
+ async run() {
17
+ const { args, flags } = await this.parse(Querychangelog);
18
+ this.setParsedFlags(flags);
19
+ const apiKey = this.getApiKey();
20
+ if (!apiKey)
21
+ this.error('Not logged in. Run: trillboards login');
22
+ let urlPath = '/v1/changelog';
23
+ const params = {};
24
+ if (flags['since'])
25
+ params['since'] = flags['since'];
26
+ if (flags['type'])
27
+ params['type'] = flags['type'];
28
+ const result = await apiRequest('GET', urlPath, {
29
+ apiKey,
30
+ baseUrl: flags['base-url'] ?? 'https://api.trillboards.com',
31
+ params: Object.keys(params).length > 0 ? params : undefined,
32
+ });
33
+ this.log(formatOutput(result, this.getFormat()));
34
+ }
35
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class Dataapiaggregate extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {};
6
+ static flags: {
7
+ group_by: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ metric: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ since: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ }