@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.
- package/README.md +1288 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/base-command.d.ts +20 -0
- package/dist/base-command.js +60 -0
- package/dist/commands/api.d.ts +14 -0
- package/dist/commands/api.js +80 -0
- package/dist/commands/audience/live.d.ts +14 -0
- package/dist/commands/audience/live.js +117 -0
- package/dist/commands/billing/usage.d.ts +10 -0
- package/dist/commands/billing/usage.js +92 -0
- package/dist/commands/billing.d.ts +6 -0
- package/dist/commands/billing.js +49 -0
- package/dist/commands/debug.d.ts +10 -0
- package/dist/commands/debug.js +156 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.js +145 -0
- package/dist/commands/earnings.d.ts +15 -0
- package/dist/commands/earnings.js +182 -0
- package/dist/commands/endpoints/index.d.ts +9 -0
- package/dist/commands/endpoints/index.js +35 -0
- package/dist/commands/errors/index.d.ts +9 -0
- package/dist/commands/errors/index.js +34 -0
- package/dist/commands/fill-rate.d.ts +12 -0
- package/dist/commands/fill-rate.js +141 -0
- package/dist/commands/generated/generated-changelog-query.d.ts +14 -0
- package/dist/commands/generated/generated-changelog-query.js +35 -0
- package/dist/commands/generated/generated-data-api-aggregate.d.ts +15 -0
- package/dist/commands/generated/generated-data-api-aggregate.js +38 -0
- package/dist/commands/generated/generated-data-api-list-screens.d.ts +16 -0
- package/dist/commands/generated/generated-data-api-list-screens.js +41 -0
- package/dist/commands/generated/generated-data-api-lookalike-screens.d.ts +15 -0
- package/dist/commands/generated/generated-data-api-lookalike-screens.js +38 -0
- package/dist/commands/generated/generated-data-api-query-dsl.d.ts +13 -0
- package/dist/commands/generated/generated-data-api-query-dsl.js +32 -0
- package/dist/commands/generated/generated-data-api-screen-audience-history.d.ts +17 -0
- package/dist/commands/generated/generated-data-api-screen-audience-history.js +41 -0
- package/dist/commands/generated/generated-data-api-screen-audience-live.d.ts +14 -0
- package/dist/commands/generated/generated-data-api-screen-audience-live.js +32 -0
- package/dist/commands/generated/generated-data-api-semantic-search.d.ts +16 -0
- package/dist/commands/generated/generated-data-api-semantic-search.js +41 -0
- package/dist/commands/generated/generated-data-api-stats.d.ts +12 -0
- package/dist/commands/generated/generated-data-api-stats.js +26 -0
- package/dist/commands/generated/generated-data-api-stream.d.ts +14 -0
- package/dist/commands/generated/generated-data-api-stream.js +35 -0
- package/dist/commands/generated/generated-data-api-usage.d.ts +13 -0
- package/dist/commands/generated/generated-data-api-usage.js +32 -0
- package/dist/commands/generated/generated-device-ota-manifest.d.ts +13 -0
- package/dist/commands/generated/generated-device-ota-manifest.js +32 -0
- package/dist/commands/generated/generated-example-healthcheck.d.ts +13 -0
- package/dist/commands/generated/generated-example-healthcheck.js +32 -0
- package/dist/commands/generated/generated-playground-dry-run.d.ts +14 -0
- package/dist/commands/generated/generated-playground-dry-run.js +32 -0
- package/dist/commands/generated/generated-request-metadata-get.d.ts +14 -0
- package/dist/commands/generated/generated-request-metadata-get.js +32 -0
- package/dist/commands/generated/generated-sdk-enrich.d.ts +13 -0
- package/dist/commands/generated/generated-sdk-enrich.js +32 -0
- package/dist/commands/generated/generated-sdk-impression.d.ts +13 -0
- package/dist/commands/generated/generated-sdk-impression.js +32 -0
- package/dist/commands/generated/generated-sdk-qr-beacon.d.ts +13 -0
- package/dist/commands/generated/generated-sdk-qr-beacon.js +32 -0
- package/dist/commands/generated/generated-sdk-qr-scan.d.ts +13 -0
- package/dist/commands/generated/generated-sdk-qr-scan.js +32 -0
- package/dist/commands/generated/generated-sensing-paired-training-upload.d.ts +13 -0
- package/dist/commands/generated/generated-sensing-paired-training-upload.js +32 -0
- package/dist/commands/listen.d.ts +39 -0
- package/dist/commands/listen.js +245 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +68 -0
- package/dist/commands/logs/tail.d.ts +47 -0
- package/dist/commands/logs/tail.js +240 -0
- package/dist/commands/reconcile.d.ts +9 -0
- package/dist/commands/reconcile.js +58 -0
- package/dist/commands/sandbox/create.d.ts +6 -0
- package/dist/commands/sandbox/create.js +35 -0
- package/dist/commands/sandbox/emit.d.ts +12 -0
- package/dist/commands/sandbox/emit.js +57 -0
- package/dist/commands/sandbox/reset.d.ts +6 -0
- package/dist/commands/sandbox/reset.js +34 -0
- package/dist/commands/sandbox/status.d.ts +6 -0
- package/dist/commands/sandbox/status.js +58 -0
- package/dist/commands/screens/watch.d.ts +14 -0
- package/dist/commands/screens/watch.js +158 -0
- package/dist/commands/trigger.d.ts +9 -0
- package/dist/commands/trigger.js +53 -0
- package/dist/commands/validate.d.ts +12 -0
- package/dist/commands/validate.js +93 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +39 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +32 -0
- package/dist/format-helpers.d.ts +46 -0
- package/dist/format-helpers.js +96 -0
- package/dist/http.d.ts +13 -0
- package/dist/http.js +51 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.js +78 -0
- package/oclif.manifest.json +2697 -0
- package/package.json +94 -0
package/bin/dev.cmd
ADDED
package/bin/dev.js
ADDED
package/bin/run.cmd
ADDED
package/bin/run.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type OutputFormat } from './output.js';
|
|
3
|
+
export declare abstract class BaseCommand extends Command {
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static baseFlags: {
|
|
6
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
format: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
private parsedFlags?;
|
|
11
|
+
protected catch(error: Error & {
|
|
12
|
+
exitCode?: number;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
protected getApiKey(): string;
|
|
15
|
+
protected getBaseUrl(): string;
|
|
16
|
+
protected getFormat(): OutputFormat;
|
|
17
|
+
protected output(data: unknown): void;
|
|
18
|
+
protected parseGlobalFlags(): Promise<void>;
|
|
19
|
+
protected setParsedFlags(flags: Record<string, unknown>): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readConfig } from './config.js';
|
|
3
|
+
import { TrillboardsApiError } from './http.js';
|
|
4
|
+
import { formatOutput } from './output.js';
|
|
5
|
+
export class BaseCommand extends Command {
|
|
6
|
+
static enableJsonFlag = false;
|
|
7
|
+
static baseFlags = {
|
|
8
|
+
'api-key': Flags.string({
|
|
9
|
+
description: 'API key (or set TRILLBOARDS_API_KEY env var)',
|
|
10
|
+
env: 'TRILLBOARDS_API_KEY',
|
|
11
|
+
}),
|
|
12
|
+
'base-url': Flags.string({
|
|
13
|
+
default: 'https://api.trillboards.com',
|
|
14
|
+
description: 'API base URL',
|
|
15
|
+
}),
|
|
16
|
+
format: Flags.string({
|
|
17
|
+
description: 'Output format',
|
|
18
|
+
options: ['json', 'table', 'yaml'],
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
parsedFlags;
|
|
22
|
+
async catch(error) {
|
|
23
|
+
if (error instanceof TrillboardsApiError) {
|
|
24
|
+
this.error(`${error.message}\n\nRun: trillboards errors ${error.code}`, { exit: 1 });
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
getApiKey() {
|
|
29
|
+
const flags = this.parsedFlags;
|
|
30
|
+
return (flags?.['api-key'] ||
|
|
31
|
+
process.env.TRILLBOARDS_API_KEY ||
|
|
32
|
+
readConfig()?.api_key ||
|
|
33
|
+
'');
|
|
34
|
+
}
|
|
35
|
+
getBaseUrl() {
|
|
36
|
+
const flags = this.parsedFlags;
|
|
37
|
+
return flags?.['base-url'] || 'https://api.trillboards.com';
|
|
38
|
+
}
|
|
39
|
+
getFormat() {
|
|
40
|
+
const flags = this.parsedFlags;
|
|
41
|
+
const explicit = flags?.format;
|
|
42
|
+
if (explicit)
|
|
43
|
+
return explicit;
|
|
44
|
+
const configFormat = readConfig()?.default_format;
|
|
45
|
+
if (configFormat)
|
|
46
|
+
return configFormat;
|
|
47
|
+
return process.stdout.isTTY ? 'table' : 'json';
|
|
48
|
+
}
|
|
49
|
+
output(data) {
|
|
50
|
+
const formatted = formatOutput(data, this.getFormat());
|
|
51
|
+
this.log(formatted);
|
|
52
|
+
}
|
|
53
|
+
async parseGlobalFlags() {
|
|
54
|
+
// oclif parses flags in run() — this is called from each command's run() after parse()
|
|
55
|
+
// Individual commands set parsedFlags after calling this.parse()
|
|
56
|
+
}
|
|
57
|
+
setParsedFlags(flags) {
|
|
58
|
+
this.parsedFlags = flags;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class Api extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
method: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
jq: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import { apiRequest } from '../http.js';
|
|
4
|
+
export default class Api extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
method: Args.string({
|
|
7
|
+
description: 'HTTP method (GET, POST, PUT, PATCH, DELETE)',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
path: Args.string({
|
|
11
|
+
description: 'API path (e.g. /v1/partner/screens)',
|
|
12
|
+
required: true,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static description = 'Make raw API requests (like gh api)';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> api GET /v1/partner/screens',
|
|
18
|
+
'<%= config.bin %> api POST /v1/partner/device --data \'{"name":"test"}\'',
|
|
19
|
+
'<%= config.bin %> api GET /v1/partner/info --jq partner_name',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
data: Flags.string({ char: 'd', description: 'Request body (JSON string)' }),
|
|
23
|
+
jq: Flags.string({ description: 'JSON path expression to extract (e.g. "data.items" or "name")' }),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { args, flags } = await this.parse(Api);
|
|
27
|
+
this.setParsedFlags(flags);
|
|
28
|
+
const apiKey = this.getApiKey();
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
this.error('Not logged in. Run: trillboards login', { exit: 1 });
|
|
31
|
+
}
|
|
32
|
+
const method = args.method.toUpperCase();
|
|
33
|
+
const validMethods = ['DELETE', 'GET', 'PATCH', 'POST', 'PUT'];
|
|
34
|
+
if (!validMethods.includes(method)) {
|
|
35
|
+
this.error(`Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(', ')}`, { exit: 1 });
|
|
36
|
+
}
|
|
37
|
+
let body;
|
|
38
|
+
if (flags.data) {
|
|
39
|
+
try {
|
|
40
|
+
body = JSON.parse(flags.data);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
this.error('Invalid JSON in --data flag.', { exit: 1 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const baseUrl = this.getBaseUrl();
|
|
47
|
+
const result = await apiRequest(method, args.path, { apiKey, baseUrl, body });
|
|
48
|
+
// Apply jq-like path extraction
|
|
49
|
+
if (flags.jq) {
|
|
50
|
+
const extracted = jqPath(result, flags.jq);
|
|
51
|
+
this.output(extracted);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.output(result);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Simple jq-like path evaluator.
|
|
60
|
+
* Supports dot-separated paths: "data.items", "name", "data.0.id"
|
|
61
|
+
*/
|
|
62
|
+
function jqPath(data, path) {
|
|
63
|
+
const parts = path.split('.');
|
|
64
|
+
let current = data;
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
if (current === null || current === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (typeof current !== 'object')
|
|
69
|
+
return undefined;
|
|
70
|
+
// Array index
|
|
71
|
+
const index = Number.parseInt(part, 10);
|
|
72
|
+
if (Array.isArray(current) && !Number.isNaN(index)) {
|
|
73
|
+
current = current[index];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
current = current[part];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class AudienceLive extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
screen_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private fetchAudience;
|
|
13
|
+
private renderAudience;
|
|
14
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
import { clearScreen, colorize, relativeTime } from '../../format-helpers.js';
|
|
4
|
+
import { apiRequest } from '../../http.js';
|
|
5
|
+
export default class AudienceLive extends BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
screen_id: Args.string({
|
|
8
|
+
description: 'Screen ID to monitor',
|
|
9
|
+
required: true,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Real-time audience sensing data for a screen';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> audience live scr_abc123',
|
|
15
|
+
'<%= config.bin %> audience live scr_abc123 --format json',
|
|
16
|
+
'<%= config.bin %> audience live scr_abc123 --interval 3',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
interval: Flags.integer({
|
|
20
|
+
default: 5,
|
|
21
|
+
description: 'Polling interval in seconds',
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { args, flags } = await this.parse(AudienceLive);
|
|
26
|
+
this.setParsedFlags(flags);
|
|
27
|
+
const apiKey = this.getApiKey();
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
this.error('Not logged in. Run: trillboards login', { exit: 1 });
|
|
30
|
+
}
|
|
31
|
+
const baseUrl = this.getBaseUrl();
|
|
32
|
+
const screenId = args.screen_id;
|
|
33
|
+
const format = this.getFormat();
|
|
34
|
+
const intervalSec = flags.interval ?? 5;
|
|
35
|
+
// Single fetch for JSON format
|
|
36
|
+
if (format === 'json' && !process.stdout.isTTY) {
|
|
37
|
+
const data = await this.fetchAudience(apiKey, baseUrl, screenId);
|
|
38
|
+
this.output(data);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let lastFetchTime = new Date();
|
|
42
|
+
const render = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const data = await this.fetchAudience(apiKey, baseUrl, screenId);
|
|
45
|
+
lastFetchTime = new Date();
|
|
46
|
+
if (format === 'json') {
|
|
47
|
+
this.log(JSON.stringify({ ...data, fetched_at: lastFetchTime.toISOString() }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
clearScreen();
|
|
51
|
+
this.renderAudience(screenId, data, intervalSec, lastFetchTime);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (format !== 'json') {
|
|
55
|
+
clearScreen();
|
|
56
|
+
}
|
|
57
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
58
|
+
if (msg.includes('404') || msg.includes('not found')) {
|
|
59
|
+
this.log(colorize(`Screen: ${screenId} -- Live Audience`, 'bold'));
|
|
60
|
+
this.log('');
|
|
61
|
+
this.log(colorize('Audience sensing not available for this screen.', 'dim'));
|
|
62
|
+
this.log('This screen may not have sensing hardware configured.');
|
|
63
|
+
this.log('');
|
|
64
|
+
this.log(colorize(`Retrying every ${intervalSec}s... Press Ctrl+C to stop`, 'dim'));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.log(colorize(`Error: ${msg}`, 'red'));
|
|
68
|
+
this.log('Retrying...');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// Initial render
|
|
73
|
+
await render();
|
|
74
|
+
// Schedule refresh
|
|
75
|
+
const timer = setInterval(() => {
|
|
76
|
+
render().catch(() => {
|
|
77
|
+
// Errors handled inside render
|
|
78
|
+
});
|
|
79
|
+
}, intervalSec * 1000);
|
|
80
|
+
// Clean exit
|
|
81
|
+
const cleanup = () => {
|
|
82
|
+
clearInterval(timer);
|
|
83
|
+
this.log('\nStopped audience monitoring.');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
process.on('SIGINT', cleanup);
|
|
87
|
+
process.on('SIGTERM', cleanup);
|
|
88
|
+
// Keep process alive
|
|
89
|
+
await new Promise(() => {
|
|
90
|
+
// Never resolves — exits via signal handler
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async fetchAudience(apiKey, baseUrl, screenId) {
|
|
94
|
+
return apiRequest('GET', `/v1/partner/audience/live/${screenId}`, { apiKey, baseUrl });
|
|
95
|
+
}
|
|
96
|
+
renderAudience(screenId, data, intervalSec, lastFetch) {
|
|
97
|
+
this.log(colorize(`Screen: ${screenId} -- Live Audience`, 'bold'));
|
|
98
|
+
this.log('');
|
|
99
|
+
const labelW = 14;
|
|
100
|
+
const lines = [
|
|
101
|
+
['Faces:', String(data.faces ?? '?')],
|
|
102
|
+
['Attention:', data.attention != null ? data.attention.toFixed(2) : '?'],
|
|
103
|
+
['Mood:', data.mood ?? '?'],
|
|
104
|
+
['Dwell:', data.dwell_seconds != null ? `${data.dwell_seconds}s` : '?'],
|
|
105
|
+
['Intent:', data.intent ?? '?'],
|
|
106
|
+
['Density:', String(data.density ?? '?')],
|
|
107
|
+
['Income:', data.income ?? '?'],
|
|
108
|
+
['Segment:', data.segment ?? '?'],
|
|
109
|
+
];
|
|
110
|
+
for (const [label, value] of lines) {
|
|
111
|
+
this.log(`${label.padEnd(labelW)} ${value}`);
|
|
112
|
+
}
|
|
113
|
+
this.log('');
|
|
114
|
+
this.log(colorize(`Last updated: ${relativeTime(lastFetch)} (refreshing every ${intervalSec}s)`, 'dim'));
|
|
115
|
+
this.log(colorize('Press Ctrl+C to stop', 'dim'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class BillingUsage extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
breakdown: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private renderBreakdown;
|
|
10
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
import { colorize, formatCount, formatCurrency } from '../../format-helpers.js';
|
|
4
|
+
import { apiRequest } from '../../http.js';
|
|
5
|
+
export default class BillingUsage extends BaseCommand {
|
|
6
|
+
static description = 'Billing usage details with optional per-product breakdown';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> billing usage',
|
|
9
|
+
'<%= config.bin %> billing usage --breakdown',
|
|
10
|
+
'<%= config.bin %> billing usage --format json',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
breakdown: Flags.boolean({
|
|
14
|
+
default: false,
|
|
15
|
+
description: 'Show per-product breakdown table',
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(BillingUsage);
|
|
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 data = await apiRequest('GET', '/v1/partner/billing/usage-summary', { apiKey, baseUrl });
|
|
28
|
+
if (format === 'json') {
|
|
29
|
+
this.output(data);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.log(colorize('Billing Usage', 'bold'));
|
|
33
|
+
this.log('');
|
|
34
|
+
this.log(`Plan: ${data.current_plan ?? 'N/A'}`);
|
|
35
|
+
if (data.period_start && data.period_end) {
|
|
36
|
+
this.log(`Period: ${data.period_start} to ${data.period_end}`);
|
|
37
|
+
}
|
|
38
|
+
if (data.screens_active != null) {
|
|
39
|
+
const limit = data.screens_limit != null ? `/${data.screens_limit}` : '';
|
|
40
|
+
this.log(`Screens: ${data.screens_active}${limit}`);
|
|
41
|
+
}
|
|
42
|
+
if (data.impressions_used != null) {
|
|
43
|
+
const limit = data.impressions_limit ? `/${formatCount(data.impressions_limit)}` : '';
|
|
44
|
+
this.log(`Impressions: ${formatCount(data.impressions_used)}${limit}`);
|
|
45
|
+
if (data.impressions_limit && data.impressions_used > data.impressions_limit) {
|
|
46
|
+
const overage = data.impressions_used - data.impressions_limit;
|
|
47
|
+
this.log(colorize(` Overage: ${formatCount(overage)} impressions over limit`, 'yellow'));
|
|
48
|
+
if (data.overage_rate != null) {
|
|
49
|
+
this.log(` Overage Rate: ${formatCurrency(data.overage_rate)}/1000`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (data.total_cost != null) {
|
|
54
|
+
this.log(`Total Cost: ${formatCurrency(data.total_cost)}`);
|
|
55
|
+
}
|
|
56
|
+
// Breakdown table
|
|
57
|
+
if (flags.breakdown) {
|
|
58
|
+
this.log('');
|
|
59
|
+
this.renderBreakdown(data.products ?? []);
|
|
60
|
+
}
|
|
61
|
+
if (data.next_billing_date) {
|
|
62
|
+
this.log('');
|
|
63
|
+
this.log(`Next billing date: ${data.next_billing_date}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
renderBreakdown(products) {
|
|
67
|
+
this.log(colorize('Per-Product Breakdown', 'bold'));
|
|
68
|
+
this.log('');
|
|
69
|
+
if (products.length === 0) {
|
|
70
|
+
this.log(' No product breakdown available.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const nameW = 28;
|
|
74
|
+
const qtyW = 14;
|
|
75
|
+
const unitW = 14;
|
|
76
|
+
const costW = 14;
|
|
77
|
+
this.log([
|
|
78
|
+
'Product'.padEnd(nameW),
|
|
79
|
+
'Quantity'.padEnd(qtyW),
|
|
80
|
+
'Unit'.padEnd(unitW),
|
|
81
|
+
'Cost'.padEnd(costW),
|
|
82
|
+
].join(' '));
|
|
83
|
+
this.log('-'.repeat(nameW + qtyW + unitW + costW + 6));
|
|
84
|
+
for (const product of products) {
|
|
85
|
+
const name = (product.name ?? 'Unknown').slice(0, nameW).padEnd(nameW);
|
|
86
|
+
const qty = product.quantity != null ? formatCount(product.quantity).padEnd(qtyW) : 'N/A'.padEnd(qtyW);
|
|
87
|
+
const unit = (product.unit ?? '').padEnd(unitW);
|
|
88
|
+
const cost = product.cost != null ? formatCurrency(product.cost).padEnd(costW) : 'N/A'.padEnd(costW);
|
|
89
|
+
this.log(`${name} ${qty} ${unit} ${cost}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import { colorize, formatCount, formatCurrency } from '../format-helpers.js';
|
|
3
|
+
import { apiRequest } from '../http.js';
|
|
4
|
+
export default class Billing extends BaseCommand {
|
|
5
|
+
static description = 'Billing summary — current plan and usage';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> billing',
|
|
8
|
+
'<%= config.bin %> billing --format json',
|
|
9
|
+
];
|
|
10
|
+
async run() {
|
|
11
|
+
const { flags } = await this.parse(Billing);
|
|
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 data = await apiRequest('GET', '/v1/partner/billing/usage-summary', { apiKey, baseUrl });
|
|
20
|
+
if (format === 'json') {
|
|
21
|
+
this.output(data);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.log(colorize('Billing Summary', 'bold'));
|
|
25
|
+
this.log('');
|
|
26
|
+
this.log(`Plan: ${data.current_plan ?? 'N/A'}`);
|
|
27
|
+
if (data.screens_active != null && data.screens_limit != null) {
|
|
28
|
+
this.log(`Screens: ${data.screens_active}/${data.screens_limit}`);
|
|
29
|
+
}
|
|
30
|
+
else if (data.screens_active != null) {
|
|
31
|
+
this.log(`Screens: ${data.screens_active}`);
|
|
32
|
+
}
|
|
33
|
+
if (data.impressions_used != null) {
|
|
34
|
+
const limit = data.impressions_limit ? `/${formatCount(data.impressions_limit)}` : '';
|
|
35
|
+
this.log(`Impressions: ${formatCount(data.impressions_used)}${limit}`);
|
|
36
|
+
}
|
|
37
|
+
if (data.total_cost != null) {
|
|
38
|
+
this.log(`Current Cost: ${formatCurrency(data.total_cost)}`);
|
|
39
|
+
}
|
|
40
|
+
if (data.period_start && data.period_end) {
|
|
41
|
+
this.log(`Period: ${data.period_start} to ${data.period_end}`);
|
|
42
|
+
}
|
|
43
|
+
if (data.next_billing_date) {
|
|
44
|
+
this.log(`Next Billing: ${data.next_billing_date}`);
|
|
45
|
+
}
|
|
46
|
+
this.log('');
|
|
47
|
+
this.log('For detailed breakdown: trillboards billing usage --breakdown');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class Debug extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
screen_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private renderDiagnostic;
|
|
10
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import { colorFillRate, colorize, formatCount, formatCurrency, relativeTime } from '../format-helpers.js';
|
|
4
|
+
import { apiRequest } from '../http.js';
|
|
5
|
+
export default class Debug extends BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
screen_id: Args.string({
|
|
8
|
+
description: 'Screen ID to diagnose',
|
|
9
|
+
required: true,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Diagnose a screen — status, performance, audience, issues';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> debug scr_abc123',
|
|
15
|
+
'<%= config.bin %> debug scr_abc123 --format json',
|
|
16
|
+
];
|
|
17
|
+
async run() {
|
|
18
|
+
const { args, flags } = await this.parse(Debug);
|
|
19
|
+
this.setParsedFlags(flags);
|
|
20
|
+
const apiKey = this.getApiKey();
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
this.error('Not logged in. Run: trillboards login', { exit: 1 });
|
|
23
|
+
}
|
|
24
|
+
const baseUrl = this.getBaseUrl();
|
|
25
|
+
const screenId = args.screen_id;
|
|
26
|
+
const format = this.getFormat();
|
|
27
|
+
// Make 4 parallel API calls
|
|
28
|
+
const [healthResult, analyticsResult, audienceResult, fleetResult] = await Promise.allSettled([
|
|
29
|
+
apiRequest('GET', '/v1/partner/connect/health', { apiKey, baseUrl }),
|
|
30
|
+
apiRequest('GET', `/v1/partner/screens/${screenId}/analytics`, {
|
|
31
|
+
apiKey,
|
|
32
|
+
baseUrl,
|
|
33
|
+
params: { period: '1d' },
|
|
34
|
+
}),
|
|
35
|
+
apiRequest('GET', `/v1/partner/audience/live/${screenId}`, { apiKey, baseUrl }),
|
|
36
|
+
apiRequest('GET', '/v1/partner/fleet/analytics', { apiKey, baseUrl }),
|
|
37
|
+
]);
|
|
38
|
+
// Extract screen from health list
|
|
39
|
+
const health = healthResult.status === 'fulfilled' ? healthResult.value : null;
|
|
40
|
+
const screen = health?.screens?.find((s) => s.screen_id === screenId || s.name === screenId);
|
|
41
|
+
const analytics = analyticsResult.status === 'fulfilled' ? analyticsResult.value : null;
|
|
42
|
+
const audience = audienceResult.status === 'fulfilled' ? audienceResult.value : null;
|
|
43
|
+
const fleet = fleetResult.status === 'fulfilled' ? fleetResult.value : null;
|
|
44
|
+
if (format === 'json') {
|
|
45
|
+
this.output({
|
|
46
|
+
audience: audience ?? null,
|
|
47
|
+
fleet: fleet ?? null,
|
|
48
|
+
health: screen ?? null,
|
|
49
|
+
screen_analytics: analytics ?? null,
|
|
50
|
+
screen_id: screenId,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.renderDiagnostic(screenId, screen, analytics, audience, fleet);
|
|
55
|
+
}
|
|
56
|
+
renderDiagnostic(screenId, screen, analytics, audience, fleet) {
|
|
57
|
+
const screenName = screen?.name ?? screenId;
|
|
58
|
+
this.log(colorize(`Screen: ${screenName} (${screenId})`, 'bold'));
|
|
59
|
+
this.log('');
|
|
60
|
+
const issues = [];
|
|
61
|
+
const suggestions = [];
|
|
62
|
+
// STATUS
|
|
63
|
+
if (screen) {
|
|
64
|
+
const status = screen.status?.toLowerCase() ?? 'unknown';
|
|
65
|
+
if (status === 'online') {
|
|
66
|
+
const hbText = screen.last_heartbeat ? relativeTime(screen.last_heartbeat) : 'unknown';
|
|
67
|
+
this.log(`STATUS: ${colorize('\u2713', 'green')} Online (last heartbeat ${hbText})`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.log(`STATUS: ${colorize('\u2717', 'red')} ${screen.status ?? 'Offline'}`);
|
|
71
|
+
issues.push('Screen is offline');
|
|
72
|
+
suggestions.push('Check device power and network connectivity');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.log(`STATUS: ${colorize('\u2013', 'yellow')} Could not fetch health data`);
|
|
77
|
+
}
|
|
78
|
+
// FILL RATE
|
|
79
|
+
if (analytics?.fill_rate != null) {
|
|
80
|
+
const networkAvg = fleet?.fill_rate ?? 0;
|
|
81
|
+
const diff = networkAvg > 0 ? analytics.fill_rate - networkAvg : 0;
|
|
82
|
+
if (analytics.fill_rate >= 70) {
|
|
83
|
+
this.log(`FILL RATE: ${colorize('\u2713', 'green')} ${colorFillRate(analytics.fill_rate)} (network avg: ${networkAvg}%)`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const symbol = analytics.fill_rate >= 50 ? '\u26A0' : '\u2717';
|
|
87
|
+
const color = analytics.fill_rate >= 50 ? 'yellow' : 'red';
|
|
88
|
+
this.log(`FILL RATE: ${colorize(symbol, color)} ${colorFillRate(analytics.fill_rate)} (network avg: ${networkAvg}%)`);
|
|
89
|
+
if (diff < -10) {
|
|
90
|
+
issues.push(`Fill rate below network average by ${Math.abs(Math.round(diff))} points`);
|
|
91
|
+
suggestions.push('Run: trillboards fill-rate --by partner');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.log(`FILL RATE: ${colorize('\u2013', 'yellow')} Not available`);
|
|
97
|
+
}
|
|
98
|
+
// IMPRESSIONS
|
|
99
|
+
if (analytics?.impressions != null) {
|
|
100
|
+
const fleetAvgImpressions = fleet?.impressions ?? 0;
|
|
101
|
+
if (analytics.impressions > 0) {
|
|
102
|
+
this.log(`IMPRESSIONS: ${colorize('\u2713', 'green')} ${formatCount(analytics.impressions)} today`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.log(`IMPRESSIONS: ${colorize('\u2717', 'red')} 0 today`);
|
|
106
|
+
issues.push('No impressions recorded today');
|
|
107
|
+
if (fleetAvgImpressions > 0) {
|
|
108
|
+
suggestions.push('Check if programmatic partners are active');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.log(`IMPRESSIONS: ${colorize('\u2013', 'yellow')} Not available`);
|
|
114
|
+
}
|
|
115
|
+
// LAST AD
|
|
116
|
+
if (screen?.last_ad_at) {
|
|
117
|
+
const lastAdTime = relativeTime(screen.last_ad_at);
|
|
118
|
+
this.log(`LAST AD: ${colorize('\u2713', 'green')} ${lastAdTime}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.log(`LAST AD: ${colorize('\u2013', 'yellow')} Not available`);
|
|
122
|
+
}
|
|
123
|
+
// AUDIENCE
|
|
124
|
+
if (audience) {
|
|
125
|
+
const facesStr = audience.faces != null ? String(audience.faces) : '?';
|
|
126
|
+
const attnStr = audience.attention != null ? audience.attention.toFixed(2) : '?';
|
|
127
|
+
this.log(`AUDIENCE: ${colorize('\u2713', 'green')} ${facesStr} faces, attention ${attnStr}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.log(`AUDIENCE: ${colorize('\u2013', 'dim')} Not available (no sensing configured)`);
|
|
131
|
+
}
|
|
132
|
+
// REVENUE
|
|
133
|
+
if (analytics?.revenue != null && analytics.revenue > 0) {
|
|
134
|
+
this.log(`REVENUE: ${colorize('\u2713', 'green')} ${formatCurrency(analytics.revenue)} today`);
|
|
135
|
+
}
|
|
136
|
+
// Issues summary
|
|
137
|
+
this.log('');
|
|
138
|
+
if (issues.length > 0) {
|
|
139
|
+
this.log(colorize('ISSUES FOUND:', 'yellow'));
|
|
140
|
+
for (const issue of issues) {
|
|
141
|
+
this.log(` ${colorize('\u26A0', 'yellow')} ${issue}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.log(colorize('No issues found.', 'green'));
|
|
146
|
+
}
|
|
147
|
+
if (suggestions.length > 0) {
|
|
148
|
+
this.log('');
|
|
149
|
+
this.log(colorize('SUGGESTED FIXES:', 'bold'));
|
|
150
|
+
for (const suggestion of suggestions) {
|
|
151
|
+
this.log(` ${suggestion}`);
|
|
152
|
+
}
|
|
153
|
+
this.log(` Run: trillboards api GET /v1/partner/screens/${screenId}/programmatic-settings`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|