@formant/formant-cli 0.2.0 → 0.3.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 +380 -116
- package/dist/commands/ingest/batch.d.ts +21 -0
- package/dist/commands/ingest/batch.js +170 -0
- package/dist/commands/ingest/batch.js.map +1 -0
- package/dist/commands/ingest/bitset.d.ts +15 -0
- package/dist/commands/ingest/bitset.js +128 -0
- package/dist/commands/ingest/bitset.js.map +1 -0
- package/dist/commands/ingest/health.d.ts +15 -0
- package/dist/commands/ingest/health.js +94 -0
- package/dist/commands/ingest/health.js.map +1 -0
- package/dist/commands/ingest/image.d.ts +15 -0
- package/dist/commands/ingest/image.js +99 -0
- package/dist/commands/ingest/image.js.map +1 -0
- package/dist/commands/ingest/json.d.ts +16 -0
- package/dist/commands/ingest/json.js +92 -0
- package/dist/commands/ingest/json.js.map +1 -0
- package/dist/commands/ingest/numeric.d.ts +16 -0
- package/dist/commands/ingest/numeric.js +83 -0
- package/dist/commands/ingest/numeric.js.map +1 -0
- package/dist/commands/ingest/text.d.ts +16 -0
- package/dist/commands/ingest/text.js +80 -0
- package/dist/commands/ingest/text.js.map +1 -0
- package/dist/commands/ingest/video.d.ts +16 -0
- package/dist/commands/ingest/video.js +120 -0
- package/dist/commands/ingest/video.js.map +1 -0
- package/dist/help.js +103 -53
- package/dist/help.js.map +1 -1
- package/dist/lib/api.d.ts +1 -1
- package/dist/lib/api.js +3 -0
- package/dist/lib/api.js.map +1 -1
- package/oclif.manifest.json +1434 -645
- package/package.json +4 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
interface IngestBatchRequest {
|
|
3
|
+
items: Array<{
|
|
4
|
+
deviceId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: 'bitset' | 'health' | 'image' | 'json' | 'numeric' | 'text' | 'video';
|
|
7
|
+
tags: Record<string, string>;
|
|
8
|
+
points: Array<[number, unknown]>;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export default class IngestBatch extends BaseCommand<typeof IngestBatch> {
|
|
12
|
+
static description: string;
|
|
13
|
+
static examples: string[];
|
|
14
|
+
static flags: {
|
|
15
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
static summary: string;
|
|
19
|
+
run(): Promise<IngestBatchRequest>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
export default class IngestBatch extends BaseCommand {
|
|
5
|
+
static description = `Ingest multiple data points in a single batch request.
|
|
6
|
+
|
|
7
|
+
Reads a JSON file (or stdin) containing a batch ingestion payload and sends all data points
|
|
8
|
+
in a single API request. This is more efficient than sending individual data points when you
|
|
9
|
+
have multiple values to ingest.
|
|
10
|
+
|
|
11
|
+
INPUT FORMAT:
|
|
12
|
+
The input JSON must have the following structure:
|
|
13
|
+
{
|
|
14
|
+
"items": [
|
|
15
|
+
{
|
|
16
|
+
"deviceId": "device-uuid", // Required: Device ID (UUID)
|
|
17
|
+
"name": "stream-name", // Required: Stream name
|
|
18
|
+
"type": "numeric|text|json|...", // Required: Data type (see below)
|
|
19
|
+
"tags": {"key": "value"}, // Optional: Tags object (string key-value pairs)
|
|
20
|
+
"points": [[timestamp_ms, value]] // Required: Array of [timestamp, value] pairs
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SUPPORTED TYPES AND VALUE FORMATS:
|
|
26
|
+
- numeric: Number value
|
|
27
|
+
Example: "points": [[1708272000000, 42.5]]
|
|
28
|
+
|
|
29
|
+
- text: String value
|
|
30
|
+
Example: "points": [[1708272000000, "hello world"]]
|
|
31
|
+
|
|
32
|
+
- json: JSON object encoded as string
|
|
33
|
+
Example: "points": [[1708272000000, "{\\"x\\":10,\\"y\\":20}"]]
|
|
34
|
+
|
|
35
|
+
- image: Object with url (required), size and annotations (optional)
|
|
36
|
+
Example: "points": [[1708272000000, {"url": "https://example.com/img.jpg", "size": 102400}]]
|
|
37
|
+
|
|
38
|
+
- video: Object with url, duration, mimeType (required), size (optional)
|
|
39
|
+
Example: "points": [[1708272000000, {"url": "https://example.com/vid.mp4", "duration": 30000, "mimeType": "video/mp4"}]]
|
|
40
|
+
|
|
41
|
+
- bitset: Object with keys array and values array (1-1000 pairs)
|
|
42
|
+
Example: "points": [[1708272000000, {"keys": ["door", "window"], "values": [true, false]}]]
|
|
43
|
+
|
|
44
|
+
- health: Object with status (required) and clockSkewMs (optional)
|
|
45
|
+
Example: "points": [[1708272000000, {"status": "operational", "clockSkewMs": 150}]]
|
|
46
|
+
Valid status values: "unknown", "operational", "offline", "error"
|
|
47
|
+
|
|
48
|
+
Each item can have multiple points (timestamp/value pairs) for the same stream.
|
|
49
|
+
Timestamps are Unix milliseconds (use Date.now() for current time).`;
|
|
50
|
+
static examples = [
|
|
51
|
+
`<%= config.bin %> ingest batch --file data.json`,
|
|
52
|
+
`<%= config.bin %> ingest batch --stdin < data.json`,
|
|
53
|
+
`cat payload.json | <%= config.bin %> ingest batch --stdin`,
|
|
54
|
+
`# Example payload.json with multiple types:
|
|
55
|
+
{
|
|
56
|
+
"items": [
|
|
57
|
+
{
|
|
58
|
+
"deviceId": "abc-123",
|
|
59
|
+
"name": "battery_level",
|
|
60
|
+
"type": "numeric",
|
|
61
|
+
"tags": {"env": "prod"},
|
|
62
|
+
"points": [[1708272000000, 42.5], [1708272060000, 41.8]]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"deviceId": "abc-123",
|
|
66
|
+
"name": "status",
|
|
67
|
+
"type": "text",
|
|
68
|
+
"tags": {"env": "prod"},
|
|
69
|
+
"points": [[1708272000000, "operational"]]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"deviceId": "abc-123",
|
|
73
|
+
"name": "health",
|
|
74
|
+
"type": "health",
|
|
75
|
+
"tags": {},
|
|
76
|
+
"points": [[1708272000000, {"status": "operational"}]]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}`,
|
|
80
|
+
];
|
|
81
|
+
static flags = {
|
|
82
|
+
file: Flags.string({
|
|
83
|
+
char: 'f',
|
|
84
|
+
description: 'Path to JSON file containing batch payload',
|
|
85
|
+
exclusive: ['stdin'],
|
|
86
|
+
}),
|
|
87
|
+
stdin: Flags.boolean({
|
|
88
|
+
description: 'Read JSON payload from stdin',
|
|
89
|
+
exclusive: ['file'],
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
static summary = 'Ingest batch data from file or stdin';
|
|
93
|
+
async run() {
|
|
94
|
+
// Ensure either --file or --stdin is specified
|
|
95
|
+
if (!this.flags.file && !this.flags.stdin) {
|
|
96
|
+
this.error('Must specify either --file <path> or --stdin');
|
|
97
|
+
}
|
|
98
|
+
// Read input
|
|
99
|
+
let inputJson;
|
|
100
|
+
if (this.flags.file) {
|
|
101
|
+
try {
|
|
102
|
+
inputJson = readFileSync(this.flags.file, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.error(`Failed to read file "${this.flags.file}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Read from stdin
|
|
110
|
+
const chunks = [];
|
|
111
|
+
for await (const chunk of process.stdin) {
|
|
112
|
+
chunks.push(chunk);
|
|
113
|
+
}
|
|
114
|
+
inputJson = Buffer.concat(chunks).toString('utf8');
|
|
115
|
+
if (!inputJson.trim()) {
|
|
116
|
+
this.error('No data received from stdin');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Parse JSON
|
|
120
|
+
let body;
|
|
121
|
+
try {
|
|
122
|
+
body = JSON.parse(inputJson);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
this.error(`Invalid JSON: ${error instanceof Error ? error.message : 'Unknown error'}. Input must be valid JSON.`);
|
|
126
|
+
}
|
|
127
|
+
// Validate structure
|
|
128
|
+
if (!body || typeof body !== 'object') {
|
|
129
|
+
this.error('Input must be a JSON object');
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(body.items)) {
|
|
132
|
+
this.error('Input must have an "items" array');
|
|
133
|
+
}
|
|
134
|
+
if (body.items.length === 0) {
|
|
135
|
+
this.error('Items array cannot be empty');
|
|
136
|
+
}
|
|
137
|
+
// Basic validation of items
|
|
138
|
+
for (const [index, item] of body.items.entries()) {
|
|
139
|
+
if (!item.deviceId) {
|
|
140
|
+
this.error(`Item ${index}: Missing required field "deviceId"`);
|
|
141
|
+
}
|
|
142
|
+
if (!item.name) {
|
|
143
|
+
this.error(`Item ${index}: Missing required field "name"`);
|
|
144
|
+
}
|
|
145
|
+
if (!item.type) {
|
|
146
|
+
this.error(`Item ${index}: Missing required field "type"`);
|
|
147
|
+
}
|
|
148
|
+
const validTypes = ['numeric', 'text', 'json', 'image', 'video', 'bitset', 'health'];
|
|
149
|
+
if (!validTypes.includes(item.type)) {
|
|
150
|
+
this.error(`Item ${index}: Invalid type "${item.type}". Must be one of: ${validTypes.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
if (!Array.isArray(item.points)) {
|
|
153
|
+
this.error(`Item ${index}: "points" must be an array`);
|
|
154
|
+
}
|
|
155
|
+
if (item.points.length === 0) {
|
|
156
|
+
this.error(`Item ${index}: "points" array cannot be empty`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Count total points
|
|
160
|
+
const totalPoints = body.items.reduce((sum, item) => sum + item.points.length, 0);
|
|
161
|
+
// Send request (expect 204 No Content)
|
|
162
|
+
await this.api('ingest', 'batch', { body, method: 'POST' });
|
|
163
|
+
if (!this.jsonEnabled()) {
|
|
164
|
+
this.log(`\n✓ Ingested batch: ${body.items.length} stream(s), ${totalPoints} data point(s) (${this.env})\n`);
|
|
165
|
+
}
|
|
166
|
+
// Return the request body for --json output
|
|
167
|
+
return body;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../../../src/commands/ingest/batch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class IngestBitset extends BaseCommand<typeof IngestBitset> {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
device: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
stream: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
keys: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
values: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
timestamp: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
static summary: string;
|
|
14
|
+
run(): Promise<Record<string, unknown>>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
export default class IngestBitset extends BaseCommand {
|
|
4
|
+
static description = `Ingest a bitset data point to a device stream.
|
|
5
|
+
|
|
6
|
+
Sends a bitset (array of named boolean values) to the specified device stream. A bitset
|
|
7
|
+
consists of parallel arrays of keys (strings) and values (booleans).
|
|
8
|
+
|
|
9
|
+
Bitsets are useful for representing sets of binary states, flags, or boolean sensor readings
|
|
10
|
+
where you have multiple named on/off states.
|
|
11
|
+
|
|
12
|
+
Constraints:
|
|
13
|
+
- Must have 1-1000 key/value pairs
|
|
14
|
+
- Keys must be max 255 characters each
|
|
15
|
+
- Keys and values arrays must have the same length`;
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> ingest bitset --device <device-id> --stream sensors --keys "door,window,motion" --values "true,false,true"',
|
|
18
|
+
'<%= config.bin %> ingest bitset --device <device-id> --stream flags --keys "active,paused,error" --values "true,false,false" --tag system=main',
|
|
19
|
+
'<%= config.bin %> ingest bitset --device <device-id> --stream states --keys "a,b" --values "false,true"',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
device: Flags.string({
|
|
23
|
+
char: 'd',
|
|
24
|
+
description: 'Device ID (UUID)',
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
stream: Flags.string({
|
|
28
|
+
char: 's',
|
|
29
|
+
description: 'Stream name',
|
|
30
|
+
required: true,
|
|
31
|
+
}),
|
|
32
|
+
keys: Flags.string({
|
|
33
|
+
char: 'k',
|
|
34
|
+
description: 'Comma-separated list of key names (1-1000 keys, max 255 chars each)',
|
|
35
|
+
required: true,
|
|
36
|
+
}),
|
|
37
|
+
values: Flags.string({
|
|
38
|
+
char: 'v',
|
|
39
|
+
description: 'Comma-separated list of boolean values (true/false, must match key count)',
|
|
40
|
+
required: true,
|
|
41
|
+
}),
|
|
42
|
+
tag: Flags.string({
|
|
43
|
+
char: 't',
|
|
44
|
+
description: 'Tag as key=value where both are strings (can be specified multiple times)',
|
|
45
|
+
multiple: true,
|
|
46
|
+
}),
|
|
47
|
+
timestamp: Flags.string({
|
|
48
|
+
description: 'Unix timestamp in milliseconds (defaults to now)',
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
static summary = 'Ingest bitset data';
|
|
52
|
+
async run() {
|
|
53
|
+
// Parse keys
|
|
54
|
+
const keys = this.flags.keys.split(',').map((k) => k.trim());
|
|
55
|
+
// Parse values
|
|
56
|
+
const valueStrings = this.flags.values.split(',').map((v) => v.trim().toLowerCase());
|
|
57
|
+
const values = [];
|
|
58
|
+
for (const v of valueStrings) {
|
|
59
|
+
if (v === 'true') {
|
|
60
|
+
values.push(true);
|
|
61
|
+
}
|
|
62
|
+
else if (v === 'false') {
|
|
63
|
+
values.push(false);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.error(`Invalid boolean value: "${v}". Must be "true" or "false".`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Validate arrays have same length
|
|
70
|
+
if (keys.length !== values.length) {
|
|
71
|
+
this.error(`Keys and values must have the same count. Got ${keys.length} keys and ${values.length} values.`);
|
|
72
|
+
}
|
|
73
|
+
// Validate count (1-1000)
|
|
74
|
+
if (keys.length < 1 || keys.length > 1000) {
|
|
75
|
+
this.error('Bitset must have between 1 and 1000 key/value pairs.');
|
|
76
|
+
}
|
|
77
|
+
// Validate key lengths
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
if (key.length > 255) {
|
|
80
|
+
this.error(`Key "${key}" exceeds maximum length of 255 characters.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Build bitset value
|
|
84
|
+
const bitsetValue = {
|
|
85
|
+
keys,
|
|
86
|
+
values,
|
|
87
|
+
};
|
|
88
|
+
// Parse tags
|
|
89
|
+
const tags = {};
|
|
90
|
+
if (this.flags.tag) {
|
|
91
|
+
for (const tag of this.flags.tag) {
|
|
92
|
+
const [tagKey, ...valueParts] = tag.split('=');
|
|
93
|
+
if (!tagKey || valueParts.length === 0) {
|
|
94
|
+
this.error(`Invalid tag format: "${tag}". Must be key=value.`);
|
|
95
|
+
}
|
|
96
|
+
tags[tagKey] = valueParts.join('=');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Parse timestamp
|
|
100
|
+
let timestamp = Date.now();
|
|
101
|
+
if (this.flags.timestamp) {
|
|
102
|
+
timestamp = Number.parseInt(this.flags.timestamp, 10);
|
|
103
|
+
if (Number.isNaN(timestamp) || timestamp <= 0) {
|
|
104
|
+
this.error(`Invalid timestamp: "${this.flags.timestamp}". Must be a positive integer.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Build request body
|
|
108
|
+
const body = {
|
|
109
|
+
items: [
|
|
110
|
+
{
|
|
111
|
+
deviceId: this.flags.device,
|
|
112
|
+
name: this.flags.stream,
|
|
113
|
+
type: 'bitset',
|
|
114
|
+
tags,
|
|
115
|
+
points: [[timestamp, bitsetValue]],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
// Send request (expect 204 No Content)
|
|
120
|
+
await this.api('ingest', 'batch', { body, method: 'POST' });
|
|
121
|
+
if (!this.jsonEnabled()) {
|
|
122
|
+
this.log(`\n✓ Ingested bitset with ${keys.length} entries to stream "${this.flags.stream}" on device ${this.flags.device} (${this.env})\n`);
|
|
123
|
+
}
|
|
124
|
+
// Return the request body for --json output
|
|
125
|
+
return body;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=bitset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitset.js","sourceRoot":"","sources":["../../../src/commands/ingest/bitset.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class IngestHealth extends BaseCommand<typeof IngestHealth> {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
device: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
stream: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
status: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'clock-skew': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
timestamp: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
static summary: string;
|
|
14
|
+
run(): Promise<Record<string, unknown>>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
export default class IngestHealth extends BaseCommand {
|
|
4
|
+
static description = `Ingest a health status data point to a device stream.
|
|
5
|
+
|
|
6
|
+
Sends device health status information to the specified stream. Health data consists of:
|
|
7
|
+
- status: One of "unknown", "operational", "offline", or "error"
|
|
8
|
+
- clockSkewMs (optional): Clock skew in milliseconds between device and server
|
|
9
|
+
|
|
10
|
+
Health streams are used to track device operational state, system health, and time synchronization.`;
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> ingest health --device <device-id> --stream system_health --status operational',
|
|
13
|
+
'<%= config.bin %> ingest health --device <device-id> --stream health --status error --tag component=motor',
|
|
14
|
+
'<%= config.bin %> ingest health --device <device-id> --stream sync --status operational --clock-skew 150',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
device: Flags.string({
|
|
18
|
+
char: 'd',
|
|
19
|
+
description: 'Device ID (UUID)',
|
|
20
|
+
required: true,
|
|
21
|
+
}),
|
|
22
|
+
stream: Flags.string({
|
|
23
|
+
char: 's',
|
|
24
|
+
description: 'Stream name',
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
status: Flags.string({
|
|
28
|
+
description: 'Health status (unknown, operational, offline, or error)',
|
|
29
|
+
options: ['unknown', 'operational', 'offline', 'error'],
|
|
30
|
+
required: true,
|
|
31
|
+
}),
|
|
32
|
+
'clock-skew': Flags.integer({
|
|
33
|
+
description: 'Clock skew in milliseconds (optional)',
|
|
34
|
+
}),
|
|
35
|
+
tag: Flags.string({
|
|
36
|
+
char: 't',
|
|
37
|
+
description: 'Tag as key=value where both are strings (can be specified multiple times)',
|
|
38
|
+
multiple: true,
|
|
39
|
+
}),
|
|
40
|
+
timestamp: Flags.string({
|
|
41
|
+
description: 'Unix timestamp in milliseconds (defaults to now)',
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
static summary = 'Ingest health status data';
|
|
45
|
+
async run() {
|
|
46
|
+
// Build health value
|
|
47
|
+
const healthValue = {
|
|
48
|
+
status: this.flags.status,
|
|
49
|
+
};
|
|
50
|
+
if (this.flags['clock-skew'] !== undefined) {
|
|
51
|
+
healthValue.clockSkewMs = this.flags['clock-skew'];
|
|
52
|
+
}
|
|
53
|
+
// Parse tags
|
|
54
|
+
const tags = {};
|
|
55
|
+
if (this.flags.tag) {
|
|
56
|
+
for (const tag of this.flags.tag) {
|
|
57
|
+
const [key, ...valueParts] = tag.split('=');
|
|
58
|
+
if (!key || valueParts.length === 0) {
|
|
59
|
+
this.error(`Invalid tag format: "${tag}". Must be key=value.`);
|
|
60
|
+
}
|
|
61
|
+
tags[key] = valueParts.join('=');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Parse timestamp
|
|
65
|
+
let timestamp = Date.now();
|
|
66
|
+
if (this.flags.timestamp) {
|
|
67
|
+
timestamp = Number.parseInt(this.flags.timestamp, 10);
|
|
68
|
+
if (Number.isNaN(timestamp) || timestamp <= 0) {
|
|
69
|
+
this.error(`Invalid timestamp: "${this.flags.timestamp}". Must be a positive integer.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Build request body
|
|
73
|
+
const body = {
|
|
74
|
+
items: [
|
|
75
|
+
{
|
|
76
|
+
deviceId: this.flags.device,
|
|
77
|
+
name: this.flags.stream,
|
|
78
|
+
type: 'health',
|
|
79
|
+
tags,
|
|
80
|
+
points: [[timestamp, healthValue]],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
// Send request (expect 204 No Content)
|
|
85
|
+
await this.api('ingest', 'batch', { body, method: 'POST' });
|
|
86
|
+
if (!this.jsonEnabled()) {
|
|
87
|
+
const skewInfo = this.flags['clock-skew'] ? `, clock skew: ${this.flags['clock-skew']}ms` : '';
|
|
88
|
+
this.log(`\n✓ Ingested health status "${this.flags.status}"${skewInfo} to stream "${this.flags.stream}" on device ${this.flags.device} (${this.env})\n`);
|
|
89
|
+
}
|
|
90
|
+
// Return the request body for --json output
|
|
91
|
+
return body;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../../src/commands/ingest/health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class IngestImage extends BaseCommand<typeof IngestImage> {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
device: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
stream: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
url: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
size: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
timestamp: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
static summary: string;
|
|
14
|
+
run(): Promise<Record<string, unknown>>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
export default class IngestImage extends BaseCommand {
|
|
4
|
+
static description = `Ingest an image URL to a device stream.
|
|
5
|
+
|
|
6
|
+
Sends an image reference (by URL) to the specified device stream. The image must be accessible
|
|
7
|
+
via HTTP/HTTPS. Formant will fetch and store the image from the provided URL.
|
|
8
|
+
|
|
9
|
+
Image streams are used for camera feeds, snapshots, diagnostic images, etc.`;
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> ingest image --device <device-id> --stream camera_front --url https://example.com/image.jpg',
|
|
12
|
+
'<%= config.bin %> ingest image --device <device-id> --stream snapshot --url https://example.com/snap.png --size 1024000',
|
|
13
|
+
'<%= config.bin %> ingest image --device <device-id> --stream detection --url https://example.com/detect.jpg --tag frame=123',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
device: Flags.string({
|
|
17
|
+
char: 'd',
|
|
18
|
+
description: 'Device ID (UUID)',
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
stream: Flags.string({
|
|
22
|
+
char: 's',
|
|
23
|
+
description: 'Stream name',
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
url: Flags.string({
|
|
27
|
+
char: 'u',
|
|
28
|
+
description: 'Image URL (must be http:// or https://)',
|
|
29
|
+
required: true,
|
|
30
|
+
}),
|
|
31
|
+
size: Flags.integer({
|
|
32
|
+
description: 'Image size in bytes (optional)',
|
|
33
|
+
}),
|
|
34
|
+
tag: Flags.string({
|
|
35
|
+
char: 't',
|
|
36
|
+
description: 'Tag as key=value where both are strings (can be specified multiple times)',
|
|
37
|
+
multiple: true,
|
|
38
|
+
}),
|
|
39
|
+
timestamp: Flags.string({
|
|
40
|
+
description: 'Unix timestamp in milliseconds (defaults to now)',
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
static summary = 'Ingest image data';
|
|
44
|
+
async run() {
|
|
45
|
+
// Validate URL format
|
|
46
|
+
if (!this.flags.url.match(/^https?:\/\//i)) {
|
|
47
|
+
this.error('Image URL must start with http:// or https://');
|
|
48
|
+
}
|
|
49
|
+
// Build image value object
|
|
50
|
+
const imageValue = {
|
|
51
|
+
url: this.flags.url,
|
|
52
|
+
};
|
|
53
|
+
if (this.flags.size !== undefined) {
|
|
54
|
+
if (this.flags.size <= 0) {
|
|
55
|
+
this.error('Image size must be a positive number');
|
|
56
|
+
}
|
|
57
|
+
imageValue.size = this.flags.size;
|
|
58
|
+
}
|
|
59
|
+
// Parse tags
|
|
60
|
+
const tags = {};
|
|
61
|
+
if (this.flags.tag) {
|
|
62
|
+
for (const tag of this.flags.tag) {
|
|
63
|
+
const [key, ...valueParts] = tag.split('=');
|
|
64
|
+
if (!key || valueParts.length === 0) {
|
|
65
|
+
this.error(`Invalid tag format: "${tag}". Must be key=value.`);
|
|
66
|
+
}
|
|
67
|
+
tags[key] = valueParts.join('=');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Parse timestamp
|
|
71
|
+
let timestamp = Date.now();
|
|
72
|
+
if (this.flags.timestamp) {
|
|
73
|
+
timestamp = Number.parseInt(this.flags.timestamp, 10);
|
|
74
|
+
if (Number.isNaN(timestamp) || timestamp <= 0) {
|
|
75
|
+
this.error(`Invalid timestamp: "${this.flags.timestamp}". Must be a positive integer.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Build request body
|
|
79
|
+
const body = {
|
|
80
|
+
items: [
|
|
81
|
+
{
|
|
82
|
+
deviceId: this.flags.device,
|
|
83
|
+
name: this.flags.stream,
|
|
84
|
+
type: 'image',
|
|
85
|
+
tags,
|
|
86
|
+
points: [[timestamp, imageValue]],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
// Send request (expect 204 No Content)
|
|
91
|
+
await this.api('ingest', 'batch', { body, method: 'POST' });
|
|
92
|
+
if (!this.jsonEnabled()) {
|
|
93
|
+
this.log(`\n✓ Ingested image ${this.flags.url} to stream "${this.flags.stream}" on device ${this.flags.device} (${this.env})\n`);
|
|
94
|
+
}
|
|
95
|
+
// Return the request body for --json output
|
|
96
|
+
return body;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../../src/commands/ingest/image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class IngestJson extends BaseCommand<typeof IngestJson> {
|
|
3
|
+
static args: {
|
|
4
|
+
value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
device: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
stream: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
timestamp: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
static summary: string;
|
|
15
|
+
run(): Promise<Record<string, unknown>>;
|
|
16
|
+
}
|