@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,68 @@
1
+ import * as readline from 'node:readline';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { updateConfig } from '../config.js';
4
+ import { TrillboardsApiError, apiRequest } from '../http.js';
5
+ export default class Login extends BaseCommand {
6
+ static description = 'Authenticate with the Trillboards API';
7
+ static examples = [
8
+ '<%= config.bin %> login',
9
+ 'TRILLBOARDS_API_KEY=trb_partner_xxx <%= config.bin %> login',
10
+ ];
11
+ async run() {
12
+ const { flags } = await this.parse(Login);
13
+ this.setParsedFlags(flags);
14
+ let apiKey = this.getApiKey();
15
+ if (!apiKey) {
16
+ apiKey = await this.promptForKey();
17
+ }
18
+ if (!apiKey) {
19
+ this.error('No API key provided. Set TRILLBOARDS_API_KEY or pass --api-key.', { exit: 1 });
20
+ }
21
+ // Validate the key
22
+ this.log('Validating API key...');
23
+ const baseUrl = this.getBaseUrl();
24
+ try {
25
+ await apiRequest('GET', '/v1/partner/ping/auth', { apiKey, baseUrl });
26
+ }
27
+ catch (error) {
28
+ if (error instanceof TrillboardsApiError && error.statusCode === 401) {
29
+ this.error('Invalid API key. Check your key and try again.', { exit: 1 });
30
+ }
31
+ throw error;
32
+ }
33
+ this.log('API key is valid.');
34
+ // Fetch partner info
35
+ let partnerName = 'Unknown';
36
+ try {
37
+ const info = (await apiRequest('GET', '/v1/partner/info', { apiKey, baseUrl }));
38
+ partnerName = info.partner_name || info.name || 'Unknown';
39
+ }
40
+ catch {
41
+ // Non-fatal — we still save the key
42
+ this.warn('Could not fetch partner info, but key is valid.');
43
+ }
44
+ // Save to config
45
+ updateConfig({
46
+ api_key: apiKey,
47
+ base_url: baseUrl,
48
+ partner_name: partnerName,
49
+ });
50
+ this.log(`Logged in as ${partnerName}. Config saved to ~/.trillboards/config.json`);
51
+ }
52
+ promptForKey() {
53
+ // In non-interactive mode, return empty
54
+ if (!process.stdin.isTTY) {
55
+ return Promise.resolve('');
56
+ }
57
+ return new Promise((resolve) => {
58
+ const rl = readline.createInterface({
59
+ input: process.stdin,
60
+ output: process.stderr,
61
+ });
62
+ rl.question('Enter your API key: ', (answer) => {
63
+ rl.close();
64
+ resolve(answer.trim());
65
+ });
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,47 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class LogsTail extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ events: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ private abortController?;
10
+ private reconnectAttempts;
11
+ private shuttingDown;
12
+ run(): Promise<void>;
13
+ /**
14
+ * Check a status_code against a list of parsed patterns.
15
+ * Patterns:
16
+ * "4xx" -> match 400-499
17
+ * "5xx" -> match 500-599
18
+ * "404" -> match exactly 404
19
+ */
20
+ static matchesStatus(statusCode: number | undefined, patterns: Array<{
21
+ exact?: number;
22
+ rangeStart?: number;
23
+ }>): boolean;
24
+ /**
25
+ * Parse a status filter string like "4xx,5xx,200" into lookup patterns.
26
+ */
27
+ static parseStatusPatterns(raw: string): Array<{
28
+ exact?: number;
29
+ rangeStart?: number;
30
+ }>;
31
+ static sleep(ms: number): Promise<void>;
32
+ /**
33
+ * Parse one or more SSE `event:` + `data:` blocks out of a raw buffer.
34
+ * Returns the list of complete envelopes and the remaining unparsed tail.
35
+ */
36
+ static parseSseBuffer(buffer: string): {
37
+ events: Array<{
38
+ event?: string;
39
+ data: string;
40
+ id?: string;
41
+ }>;
42
+ remainder: string;
43
+ };
44
+ private streamOnce;
45
+ private handleSseEvent;
46
+ private renderEvent;
47
+ }
@@ -0,0 +1,240 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { colorize } from '../../format-helpers.js';
4
+ function maxReconnectAttempts() {
5
+ const raw = process.env.TRILLBOARDS_CLI_MAX_RECONNECTS;
6
+ if (raw === undefined || raw === '')
7
+ return 10;
8
+ const n = Number.parseInt(raw, 10);
9
+ return Number.isFinite(n) ? n : 10;
10
+ }
11
+ function reconnectDelayMs() {
12
+ const raw = process.env.TRILLBOARDS_CLI_RECONNECT_DELAY_MS;
13
+ if (raw === undefined || raw === '')
14
+ return 3000;
15
+ const n = Number.parseInt(raw, 10);
16
+ return Number.isFinite(n) ? n : 3000;
17
+ }
18
+ export default class LogsTail extends BaseCommand {
19
+ static description = 'Tail real-time request logs via server-sent events';
20
+ static examples = [
21
+ '<%= config.bin %> logs tail',
22
+ '<%= config.bin %> logs tail --events error.occurred',
23
+ '<%= config.bin %> logs tail --status 4xx,5xx',
24
+ '<%= config.bin %> logs tail --events request.logged,error.occurred --status 5xx',
25
+ ];
26
+ static flags = {
27
+ events: Flags.string({
28
+ default: 'request.logged,error.occurred',
29
+ description: 'Comma-separated SSE event types to subscribe to',
30
+ }),
31
+ status: Flags.string({
32
+ description: 'Filter by status code pattern (e.g. "4xx", "5xx", "4xx,5xx", "404")',
33
+ }),
34
+ };
35
+ abortController;
36
+ reconnectAttempts = 0;
37
+ shuttingDown = false;
38
+ async run() {
39
+ const { flags } = await this.parse(LogsTail);
40
+ this.setParsedFlags(flags);
41
+ const apiKey = this.getApiKey();
42
+ if (!apiKey) {
43
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
44
+ }
45
+ const baseUrl = this.getBaseUrl();
46
+ const events = flags.events || 'request.logged,error.occurred';
47
+ const statusPatterns = flags.status ? LogsTail.parseStatusPatterns(flags.status) : undefined;
48
+ // Graceful Ctrl+C
49
+ const cleanup = () => {
50
+ this.shuttingDown = true;
51
+ if (this.abortController)
52
+ this.abortController.abort();
53
+ this.log('\nStopped tailing logs.');
54
+ process.exit(0);
55
+ };
56
+ process.on('SIGINT', cleanup);
57
+ process.on('SIGTERM', cleanup);
58
+ this.log(colorize(`Tailing logs (events: ${events})`, 'dim'));
59
+ if (statusPatterns) {
60
+ this.log(colorize(`Status filter: ${flags.status}`, 'dim'));
61
+ }
62
+ this.log(colorize('Press Ctrl+C to stop', 'dim'));
63
+ this.log('');
64
+ while (!this.shuttingDown) {
65
+ try {
66
+ await this.streamOnce(apiKey, baseUrl, events, statusPatterns);
67
+ // Stream ended without throwing (server closed it) — attempt reconnect.
68
+ }
69
+ catch (error) {
70
+ if (this.shuttingDown)
71
+ return;
72
+ const msg = error instanceof Error ? error.message : String(error);
73
+ this.log(colorize(`Stream error: ${msg}`, 'red'));
74
+ }
75
+ if (this.shuttingDown)
76
+ return;
77
+ const maxAttempts = maxReconnectAttempts();
78
+ this.reconnectAttempts += 1;
79
+ if (this.reconnectAttempts > maxAttempts) {
80
+ this.error(`Gave up after ${maxAttempts} reconnect attempts.`, { exit: 1 });
81
+ }
82
+ const delayMs = reconnectDelayMs();
83
+ this.log(colorize(`Reconnecting (${this.reconnectAttempts}/${maxAttempts}) in ${delayMs / 1000}s...`, 'yellow'));
84
+ if (delayMs > 0)
85
+ await LogsTail.sleep(delayMs);
86
+ }
87
+ }
88
+ /**
89
+ * Check a status_code against a list of parsed patterns.
90
+ * Patterns:
91
+ * "4xx" -> match 400-499
92
+ * "5xx" -> match 500-599
93
+ * "404" -> match exactly 404
94
+ */
95
+ static matchesStatus(statusCode, patterns) {
96
+ if (statusCode === undefined)
97
+ return false;
98
+ for (const p of patterns) {
99
+ if (p.exact !== undefined && p.exact === statusCode)
100
+ return true;
101
+ if (p.rangeStart !== undefined && statusCode >= p.rangeStart && statusCode < p.rangeStart + 100) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
107
+ /**
108
+ * Parse a status filter string like "4xx,5xx,200" into lookup patterns.
109
+ */
110
+ static parseStatusPatterns(raw) {
111
+ return raw.split(',').map((s) => s.trim()).filter(Boolean).map((token) => {
112
+ const m = /^(\d)xx$/i.exec(token);
113
+ if (m)
114
+ return { rangeStart: Number(m[1]) * 100 };
115
+ const n = Number(token);
116
+ if (!Number.isNaN(n) && n >= 100 && n < 1000)
117
+ return { exact: n };
118
+ return {};
119
+ }).filter((p) => p.exact !== undefined || p.rangeStart !== undefined);
120
+ }
121
+ static sleep(ms) {
122
+ return new Promise((resolve) => {
123
+ setTimeout(resolve, ms);
124
+ });
125
+ }
126
+ /**
127
+ * Parse one or more SSE `event:` + `data:` blocks out of a raw buffer.
128
+ * Returns the list of complete envelopes and the remaining unparsed tail.
129
+ */
130
+ static parseSseBuffer(buffer) {
131
+ // SSE messages are separated by `\n\n`. Handle `\r\n\r\n` defensively.
132
+ const normalized = buffer.replace(/\r\n/g, '\n');
133
+ const parts = normalized.split('\n\n');
134
+ const remainder = parts.pop() ?? '';
135
+ const events = [];
136
+ for (const chunk of parts) {
137
+ if (!chunk.trim())
138
+ continue;
139
+ // Comments: lines starting with ':' (heartbeats). Skip entirely.
140
+ const lines = chunk.split('\n').filter((l) => l && !l.startsWith(':'));
141
+ if (lines.length === 0)
142
+ continue;
143
+ let eventType;
144
+ let id;
145
+ const dataLines = [];
146
+ for (const line of lines) {
147
+ if (line.startsWith('event:'))
148
+ eventType = line.slice(6).trim();
149
+ else if (line.startsWith('id:'))
150
+ id = line.slice(3).trim();
151
+ else if (line.startsWith('data:'))
152
+ dataLines.push(line.slice(5).trim());
153
+ }
154
+ if (dataLines.length > 0) {
155
+ events.push({ data: dataLines.join('\n'), event: eventType, id });
156
+ }
157
+ }
158
+ return { events, remainder };
159
+ }
160
+ async streamOnce(apiKey, baseUrl, events, statusPatterns) {
161
+ const url = new URL('/v1/partner/events/stream', baseUrl);
162
+ url.searchParams.set('events', events);
163
+ this.abortController = new AbortController();
164
+ const response = await fetch(url.toString(), {
165
+ headers: {
166
+ 'Accept': 'text/event-stream',
167
+ 'Authorization': `Bearer ${apiKey}`,
168
+ 'User-Agent': 'trillboards-cli',
169
+ 'X-Trillboards-Client-Type': 'agent',
170
+ },
171
+ signal: this.abortController.signal,
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error(`Server returned ${response.status} ${response.statusText}`);
175
+ }
176
+ if (!response.body) {
177
+ throw new Error('Server returned an empty body');
178
+ }
179
+ // Successful connect — reset reconnect counter.
180
+ this.reconnectAttempts = 0;
181
+ const reader = response.body.getReader();
182
+ const decoder = new TextDecoder('utf-8');
183
+ let buffer = '';
184
+ try {
185
+ while (!this.shuttingDown) {
186
+ const { done, value } = await reader.read();
187
+ if (done)
188
+ break;
189
+ buffer += decoder.decode(value, { stream: true });
190
+ const { events: parsed, remainder } = LogsTail.parseSseBuffer(buffer);
191
+ buffer = remainder;
192
+ for (const envelope of parsed) {
193
+ this.handleSseEvent(envelope, statusPatterns);
194
+ }
195
+ }
196
+ }
197
+ finally {
198
+ try {
199
+ reader.releaseLock();
200
+ }
201
+ catch { /* ignore */ }
202
+ }
203
+ }
204
+ handleSseEvent(envelope, statusPatterns) {
205
+ // Skip server-side keepalives and the initial "connected" envelope
206
+ if (!envelope.event || envelope.event === 'connected' || envelope.event === 'disconnected') {
207
+ return;
208
+ }
209
+ let parsed;
210
+ try {
211
+ parsed = JSON.parse(envelope.data);
212
+ }
213
+ catch {
214
+ // Malformed line — skip rather than crash the stream
215
+ return;
216
+ }
217
+ const payload = (parsed.data || parsed);
218
+ if (statusPatterns && !LogsTail.matchesStatus(payload.status_code, statusPatterns)) {
219
+ return;
220
+ }
221
+ this.renderEvent(envelope.event, payload);
222
+ }
223
+ renderEvent(eventType, payload) {
224
+ const status = payload.status_code ?? 0;
225
+ const color = status >= 500 ? 'red' : status >= 400 ? 'yellow' : 'green';
226
+ const timestamp = payload.timestamp ?? new Date().toISOString();
227
+ const method = (payload.method ?? '---').padEnd(6);
228
+ const path = payload.path ?? '-';
229
+ const statusStr = colorize(String(status || '---').padEnd(3), color);
230
+ const duration = payload.duration_ms !== undefined ? `${payload.duration_ms}ms` : '';
231
+ const requestId = payload.request_id ? colorize(payload.request_id, 'dim') : '';
232
+ const agent = payload.is_agent && payload.agent_name ? colorize(` [${payload.agent_name}]`, 'dim') : '';
233
+ const errorTag = payload.error_code ? colorize(` (${payload.error_code})`, 'red') : '';
234
+ const line = `${colorize(timestamp, 'dim')} ${statusStr} ${method} ${path} ${colorize(duration, 'dim')} ${requestId}${agent}${errorTag}`;
235
+ this.log(line);
236
+ if (eventType === 'error.occurred' && payload.error_type) {
237
+ this.log(` ${colorize(`error_type: ${payload.error_type}`, 'dim')}`);
238
+ }
239
+ }
240
+ }
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Reconcile extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ period: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,58 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { colorize, formatCount, formatCurrency, formatPercent } from '../format-helpers.js';
4
+ import { apiRequest } from '../http.js';
5
+ export default class Reconcile extends BaseCommand {
6
+ static description = 'Revenue reconciliation — compare our numbers with partner reports';
7
+ static examples = [
8
+ '<%= config.bin %> reconcile',
9
+ '<%= config.bin %> reconcile --period 7d',
10
+ '<%= config.bin %> reconcile --format json',
11
+ ];
12
+ static flags = {
13
+ period: Flags.string({
14
+ default: '30d',
15
+ description: 'Time period for reconciliation (e.g. 7d, 30d)',
16
+ }),
17
+ };
18
+ async run() {
19
+ const { flags } = await this.parse(Reconcile);
20
+ this.setParsedFlags(flags);
21
+ const apiKey = this.getApiKey();
22
+ if (!apiKey) {
23
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
24
+ }
25
+ const baseUrl = this.getBaseUrl();
26
+ const format = this.getFormat();
27
+ const period = flags.period ?? '30d';
28
+ const data = await apiRequest('GET', '/v1/partner/fleet/analytics', {
29
+ apiKey,
30
+ baseUrl,
31
+ params: { period },
32
+ });
33
+ if (format === 'json') {
34
+ this.output({
35
+ fill_rate: data.fill_rate ?? 0,
36
+ impressions: data.impressions ?? 0,
37
+ period,
38
+ revenue: data.revenue ?? 0,
39
+ });
40
+ return;
41
+ }
42
+ this.log(colorize('Revenue Reconciliation', 'bold'));
43
+ this.log('');
44
+ const impressions = data.impressions ?? 0;
45
+ const revenue = data.revenue ?? 0;
46
+ const fillRate = data.fill_rate ?? 0;
47
+ this.log(`Our Impressions (${period}): ${formatCount(impressions)}`);
48
+ this.log(`Our Revenue (${period}): ${formatCurrency(revenue)}`);
49
+ this.log(`Fill Rate: ${formatPercent(fillRate)}`);
50
+ this.log('');
51
+ this.log(colorize('To compare with partner reports:', 'bold'));
52
+ this.log(' 1. Log in to your demand partner dashboard');
53
+ this.log(` 2. Pull the same date range (${period})`);
54
+ this.log(' 3. Compare impression counts (threshold: +/-5%)');
55
+ this.log('');
56
+ this.log(`Run: trillboards api GET /v1/partner/fleet/analytics --format json`);
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class SandboxCreate extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ import { colorize } from '../../format-helpers.js';
3
+ import { apiRequest } from '../../http.js';
4
+ export default class SandboxCreate extends BaseCommand {
5
+ static description = 'Create a sandbox test screen';
6
+ static examples = [
7
+ '<%= config.bin %> sandbox create',
8
+ '<%= config.bin %> sandbox create --format json',
9
+ ];
10
+ async run() {
11
+ const { flags } = await this.parse(SandboxCreate);
12
+ this.setParsedFlags(flags);
13
+ const apiKey = this.getApiKey();
14
+ if (!apiKey) {
15
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
16
+ }
17
+ const baseUrl = this.getBaseUrl();
18
+ const format = this.getFormat();
19
+ const result = await apiRequest('POST', '/v1/partner/sandbox/screen', { apiKey, baseUrl });
20
+ if (format === 'json') {
21
+ this.output(result);
22
+ return;
23
+ }
24
+ this.log(colorize('Sandbox screen created!', 'green'));
25
+ this.log('');
26
+ this.log(`Screen ID: ${result.screen_id ?? 'N/A'}`);
27
+ this.log(`Device ID: ${result.device_id ?? 'N/A'}`);
28
+ this.log(`Name: ${result.name ?? 'N/A'}`);
29
+ this.log(`Status: ${result.status ?? 'N/A'}`);
30
+ this.log('');
31
+ this.log('Next steps:');
32
+ this.log(` Emit an event: trillboards sandbox emit impression.recorded`);
33
+ this.log(` Check status: trillboards sandbox status`);
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class SandboxEmit extends BaseCommand {
3
+ static args: {
4
+ event_type: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,57 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { colorize } from '../../format-helpers.js';
4
+ import { apiRequest } from '../../http.js';
5
+ export default class SandboxEmit extends BaseCommand {
6
+ static args = {
7
+ event_type: Args.string({
8
+ description: 'Event type to emit (e.g. impression.recorded, heartbeat)',
9
+ required: true,
10
+ }),
11
+ };
12
+ static description = 'Emit a test event in the sandbox';
13
+ static examples = [
14
+ '<%= config.bin %> sandbox emit impression.recorded',
15
+ '<%= config.bin %> sandbox emit heartbeat',
16
+ '<%= config.bin %> sandbox emit impression.recorded --data \'{"cpm": 3.50}\'',
17
+ ];
18
+ static flags = {
19
+ data: Flags.string({ char: 'd', description: 'Additional event data (JSON string)' }),
20
+ };
21
+ async run() {
22
+ const { args, flags } = await this.parse(SandboxEmit);
23
+ this.setParsedFlags(flags);
24
+ const apiKey = this.getApiKey();
25
+ if (!apiKey) {
26
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
27
+ }
28
+ const baseUrl = this.getBaseUrl();
29
+ const format = this.getFormat();
30
+ const eventType = args.event_type;
31
+ let extraData = {};
32
+ if (flags.data) {
33
+ try {
34
+ extraData = JSON.parse(flags.data);
35
+ }
36
+ catch {
37
+ this.error('Invalid JSON in --data flag.', { exit: 1 });
38
+ }
39
+ }
40
+ const body = {
41
+ event_type: eventType,
42
+ ...extraData,
43
+ };
44
+ const result = await apiRequest('POST', '/v1/partner/sandbox/event', { apiKey, baseUrl, body });
45
+ if (format === 'json') {
46
+ this.output(result);
47
+ return;
48
+ }
49
+ this.log(colorize(`Event emitted: ${eventType}`, 'green'));
50
+ if (result.event_id) {
51
+ this.log(`Event ID: ${result.event_id}`);
52
+ }
53
+ if (result.message) {
54
+ this.log(`${result.message}`);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class SandboxReset extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ import { colorize } from '../../format-helpers.js';
3
+ import { apiRequest } from '../../http.js';
4
+ export default class SandboxReset extends BaseCommand {
5
+ static description = 'Reset the sandbox — delete all test screens and events';
6
+ static examples = [
7
+ '<%= config.bin %> sandbox reset',
8
+ '<%= config.bin %> sandbox reset --format json',
9
+ ];
10
+ async run() {
11
+ const { flags } = await this.parse(SandboxReset);
12
+ this.setParsedFlags(flags);
13
+ const apiKey = this.getApiKey();
14
+ if (!apiKey) {
15
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
16
+ }
17
+ const baseUrl = this.getBaseUrl();
18
+ const format = this.getFormat();
19
+ const result = await apiRequest('POST', '/v1/partner/sandbox/reset', { apiKey, baseUrl });
20
+ if (format === 'json') {
21
+ this.output(result);
22
+ return;
23
+ }
24
+ this.log(colorize('Sandbox reset complete.', 'green'));
25
+ if (result.screens_deleted != null) {
26
+ this.log(`Screens deleted: ${result.screens_deleted}`);
27
+ }
28
+ if (result.events_deleted != null) {
29
+ this.log(`Events deleted: ${result.events_deleted}`);
30
+ }
31
+ this.log('');
32
+ this.log('Start fresh: trillboards sandbox create');
33
+ }
34
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class SandboxStatus extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,58 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ import { colorize, colorStatus, formatCount } from '../../format-helpers.js';
3
+ import { apiRequest } from '../../http.js';
4
+ export default class SandboxStatus extends BaseCommand {
5
+ static description = 'Show sandbox status — screens and recent events';
6
+ static examples = [
7
+ '<%= config.bin %> sandbox status',
8
+ '<%= config.bin %> sandbox status --format json',
9
+ ];
10
+ async run() {
11
+ const { flags } = await this.parse(SandboxStatus);
12
+ this.setParsedFlags(flags);
13
+ const apiKey = this.getApiKey();
14
+ if (!apiKey) {
15
+ this.error('Not logged in. Run: trillboards login', { exit: 1 });
16
+ }
17
+ const baseUrl = this.getBaseUrl();
18
+ const format = this.getFormat();
19
+ const result = await apiRequest('GET', '/v1/partner/sandbox/status', { apiKey, baseUrl });
20
+ if (format === 'json') {
21
+ this.output(result);
22
+ return;
23
+ }
24
+ this.log(colorize('Sandbox Status', 'bold'));
25
+ this.log('');
26
+ // Screens
27
+ const screens = result.screens ?? [];
28
+ this.log(colorize(`Screens (${screens.length})`, 'bold'));
29
+ if (screens.length === 0) {
30
+ this.log(' No sandbox screens. Run: trillboards sandbox create');
31
+ }
32
+ else {
33
+ for (const screen of screens) {
34
+ const name = screen.name ?? screen.screen_id ?? 'Unknown';
35
+ const status = screen.status ?? 'unknown';
36
+ this.log(` ${name} ${colorStatus(status)}`);
37
+ }
38
+ }
39
+ this.log('');
40
+ // Recent events
41
+ const events = result.events ?? [];
42
+ this.log(colorize(`Recent Events (${events.length})`, 'bold'));
43
+ if (events.length === 0) {
44
+ this.log(' No events yet. Run: trillboards sandbox emit impression.recorded');
45
+ }
46
+ else {
47
+ for (const event of events.slice(0, 20)) {
48
+ const type = event.event_type ?? 'unknown';
49
+ const time = event.timestamp ?? '';
50
+ const id = event.event_id ?? '';
51
+ this.log(` ${type.padEnd(24)} ${time.padEnd(24)} ${id}`);
52
+ }
53
+ if (events.length > 20) {
54
+ this.log(colorize(` ... and ${formatCount(events.length - 20)} more`, 'dim'));
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class ScreensWatch extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ filter: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
+ sort: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ private fetchData;
12
+ private filterAndSort;
13
+ private renderTable;
14
+ }