@thinremote/thinr-cli 1.2.0-alpha.1 → 1.2.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/CHANGELOG.md +45 -1
- package/README.md +192 -16
- package/bin/thinr.js +23 -24
- package/commands/_shared.js +154 -0
- package/commands/dashboard.js +17 -0
- package/commands/device/_shared.js +17 -34
- package/commands/device/console.js +1 -0
- package/commands/device/delete.js +53 -0
- package/commands/device/exec.js +7 -6
- package/commands/device/fs.js +193 -0
- package/commands/device/index.js +9 -0
- package/commands/device/list.js +35 -23
- package/commands/device/logs.js +178 -0
- package/commands/device/property.js +30 -28
- package/commands/device/proxy.js +3 -0
- package/commands/device/resource.js +30 -29
- package/commands/device/status.js +35 -14
- package/commands/device/transfer.js +236 -0
- package/commands/device/update.js +15 -10
- package/commands/fleet/index.js +17 -0
- package/commands/fleet/upgrade.js +275 -0
- package/commands/logout.js +10 -11
- package/commands/playbook/_shared.js +12 -0
- package/commands/playbook/index.js +18 -0
- package/commands/playbook/run.js +302 -0
- package/commands/playbook/validate.js +38 -0
- package/commands/product/_shared.js +8 -0
- package/commands/product/exec.js +243 -0
- package/commands/product/fs.js +218 -0
- package/commands/product/index.js +35 -0
- package/commands/product/list.js +86 -0
- package/commands/product/logs.js +289 -0
- package/commands/product/metric.js +124 -0
- package/commands/product/playbook.js +911 -0
- package/commands/product/property.js +76 -0
- package/commands/product/props.js +125 -0
- package/commands/product/push.js +184 -0
- package/commands/product/resource.js +76 -0
- package/commands/product/status.js +213 -0
- package/commands/profile.js +76 -8
- package/dist/dashboard.js +5644 -0
- package/lib/agent-releases.js +59 -0
- package/lib/agent-versions.js +73 -0
- package/lib/alarms.js +252 -0
- package/lib/api.js +8 -2
- package/lib/auth.js +30 -17
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +199 -0
- package/lib/config.js +5 -3
- package/lib/console.js +12 -4
- package/lib/constants.js +76 -0
- package/lib/dashboard/event-stream.js +239 -0
- package/lib/dashboard/metric-aggregate.js +64 -0
- package/lib/dashboard/metric-filter.js +39 -0
- package/lib/debug-log.js +93 -0
- package/lib/device-api.js +109 -147
- package/lib/devices.js +18 -21
- package/lib/fleet-upgrade.js +106 -0
- package/lib/format.js +114 -0
- package/lib/mcp/registry.js +14 -0
- package/lib/mcp/tools-alarms.js +510 -0
- package/lib/mcp/tools-buckets.js +103 -0
- package/lib/mcp/tools-devices.js +33 -3
- package/lib/mcp/tools-exec.js +30 -17
- package/lib/mcp/tools-filesystem.js +102 -6
- package/lib/mcp/tools-install.js +153 -0
- package/lib/mcp/tools-logs.js +225 -0
- package/lib/mcp/tools-playbook.js +211 -0
- package/lib/mcp/tools-products.js +1226 -105
- package/lib/mcp/tools-profile.js +634 -0
- package/lib/mcp/tools-tokens.js +371 -0
- package/lib/monitoring.js +26 -1
- package/lib/playbook/actions.js +447 -0
- package/lib/playbook/expression.js +302 -0
- package/lib/playbook/fleet.js +175 -0
- package/lib/playbook/loader.js +247 -0
- package/lib/playbook/runner.js +300 -0
- package/lib/playbook/schema.js +209 -0
- package/lib/playbook/vars.js +209 -0
- package/lib/product/log-presets.js +196 -0
- package/lib/product/logs.js +485 -0
- package/lib/product-orchestrator.js +124 -0
- package/lib/product.js +523 -20
- package/lib/profile.js +665 -0
- package/lib/property.js +7 -40
- package/lib/proxy.js +9 -8
- package/lib/resource.js +10 -14
- package/lib/status.js +78 -37
- package/lib/tokens.js +374 -0
- package/package.json +14 -3
- package/commands/product.js +0 -407
package/lib/property.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { requireConfig } from './config.js';
|
|
3
2
|
import api from './api.js';
|
|
4
3
|
import { apiError } from './errors.js';
|
|
4
|
+
import { info, success } from './format.js';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Get device properties
|
|
8
|
-
* @param {string} deviceId - Device ID to query
|
|
9
|
-
* @returns {Promise<Object>} Device properties
|
|
10
|
-
*/
|
|
11
6
|
export async function getDeviceProperties(deviceId) {
|
|
12
7
|
const config = requireConfig();
|
|
13
8
|
|
|
@@ -21,12 +16,6 @@ export async function getDeviceProperties(deviceId) {
|
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
/**
|
|
25
|
-
* Get device property value
|
|
26
|
-
* @param {string} deviceId - Device ID to query
|
|
27
|
-
* @param {string} propertyId - Property ID to retrieve
|
|
28
|
-
* @returns {Promise<Object>} Property value
|
|
29
|
-
*/
|
|
30
19
|
export async function getDeviceProperty(deviceId, propertyId) {
|
|
31
20
|
const config = requireConfig();
|
|
32
21
|
|
|
@@ -42,16 +31,8 @@ export async function getDeviceProperty(deviceId, propertyId) {
|
|
|
42
31
|
}
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* operation is idempotent — missing properties are created, existing
|
|
48
|
-
* ones are overwritten wholesale.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} deviceId
|
|
51
|
-
* @param {string} propertyId
|
|
52
|
-
* @param {*} value JSON-serialisable value to store.
|
|
53
|
-
* @returns {Promise<Object>} The server's persisted record.
|
|
54
|
-
*/
|
|
34
|
+
// PUT so the operation is idempotent — missing properties are created,
|
|
35
|
+
// existing ones are overwritten wholesale.
|
|
55
36
|
export async function setDeviceProperty(deviceId, propertyId, value) {
|
|
56
37
|
const config = requireConfig();
|
|
57
38
|
|
|
@@ -66,14 +47,8 @@ export async function setDeviceProperty(deviceId, propertyId, value) {
|
|
|
66
47
|
}
|
|
67
48
|
}
|
|
68
49
|
|
|
69
|
-
/**
|
|
70
|
-
* Format device properties for display
|
|
71
|
-
* @param {string} deviceId - Device ID
|
|
72
|
-
* @param {Object} properties - Properties object from API
|
|
73
|
-
* @returns {string} Formatted properties
|
|
74
|
-
*/
|
|
75
50
|
export function formatDeviceProperties(deviceId, properties) {
|
|
76
|
-
let formattedProperties = '\nProperties of device ' +
|
|
51
|
+
let formattedProperties = '\nProperties of device ' + info(deviceId) + ':\n';
|
|
77
52
|
|
|
78
53
|
properties.forEach((property) => {
|
|
79
54
|
formattedProperties += `- ${property.property}\n`;
|
|
@@ -82,24 +57,16 @@ export function formatDeviceProperties(deviceId, properties) {
|
|
|
82
57
|
return formattedProperties;
|
|
83
58
|
}
|
|
84
59
|
|
|
85
|
-
/**
|
|
86
|
-
* Format device property value for display
|
|
87
|
-
* @param {string} deviceId - Device ID
|
|
88
|
-
* @param {string} propertyId - Property ID
|
|
89
|
-
* @param {Object} propertyValue - Property value from API
|
|
90
|
-
* @returns {string} Formatted property value
|
|
91
|
-
*/
|
|
92
60
|
export function formatDeviceProperty(deviceId, propertyId, propertyValue) {
|
|
93
|
-
// show json with flattened structure
|
|
94
61
|
let formattedValue =
|
|
95
62
|
'\nValue of property ' +
|
|
96
|
-
|
|
63
|
+
info(propertyId) +
|
|
97
64
|
' for device ' +
|
|
98
|
-
|
|
65
|
+
info(deviceId) +
|
|
99
66
|
':\n';
|
|
100
67
|
const flattenedValue = flatten(propertyValue);
|
|
101
68
|
for (const key in flattenedValue) {
|
|
102
|
-
formattedValue += `${key}: ${
|
|
69
|
+
formattedValue += `${key}: ${success(flattenedValue[key])}\n`;
|
|
103
70
|
}
|
|
104
71
|
return formattedValue;
|
|
105
72
|
}
|
package/lib/proxy.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import open, { apps } from 'open';
|
|
3
2
|
import ora from 'ora';
|
|
4
3
|
import { readConfig, requireConfig } from './config.js';
|
|
5
4
|
import api from './api.js';
|
|
5
|
+
import { PROXY_PORTS } from './constants.js';
|
|
6
6
|
import { apiError } from './errors.js';
|
|
7
|
+
import { info, accent, hint, warning, error as errorStyle } from './format.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Parse target argument into address and port
|
|
@@ -135,7 +136,7 @@ export async function handleProxyAction(deviceId, protocol, target, options) {
|
|
|
135
136
|
const targetProtocol = isSecure ? 'https' : 'http';
|
|
136
137
|
const targetUrl = `${targetProtocol}://${address}${port === 80 || port === 443 ? '' : `:${port}`}`;
|
|
137
138
|
spinner.succeed(
|
|
138
|
-
`HTTP proxy running at ${
|
|
139
|
+
`HTTP proxy running at ${info(proxy.url)} → ${deviceId} -> ${accent(targetUrl)}`,
|
|
139
140
|
);
|
|
140
141
|
if (options.openBrowser !== false) {
|
|
141
142
|
await openBrowser(proxy.url);
|
|
@@ -143,17 +144,17 @@ export async function handleProxyAction(deviceId, protocol, target, options) {
|
|
|
143
144
|
} else {
|
|
144
145
|
const secureInfo = isSecure ? ' [TLS]' : '';
|
|
145
146
|
spinner.succeed(
|
|
146
|
-
`${protocol.toUpperCase()} proxy running on ${
|
|
147
|
+
`${protocol.toUpperCase()} proxy running on ${info(proxy.serverHost + ':' + proxy.serverPort)} → ${deviceId} (${address}:${port})${secureInfo}`,
|
|
147
148
|
);
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
console.log(
|
|
151
|
+
console.log(hint(`Press Ctrl+C to stop`));
|
|
151
152
|
process.stdin.resume();
|
|
152
153
|
|
|
153
154
|
// Resolve when the user hits Ctrl+C — the CLI command then exits cleanly.
|
|
154
155
|
return new Promise((resolve) => {
|
|
155
156
|
process.on('SIGINT', async () => {
|
|
156
|
-
console.log(
|
|
157
|
+
console.log(warning('\nStopping proxy...'));
|
|
157
158
|
if (proxyId) await deleteProxy(proxyId);
|
|
158
159
|
resolve(0);
|
|
159
160
|
});
|
|
@@ -166,7 +167,7 @@ export async function handleProxyAction(deviceId, protocol, target, options) {
|
|
|
166
167
|
* @param {number} max - Maximum port number
|
|
167
168
|
* @returns {number} Random port number
|
|
168
169
|
*/
|
|
169
|
-
function getRandomPort(min =
|
|
170
|
+
function getRandomPort(min = PROXY_PORTS.MIN, max = PROXY_PORTS.MAX) {
|
|
170
171
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -259,7 +260,7 @@ async function deleteProxy(proxyId) {
|
|
|
259
260
|
|
|
260
261
|
return true;
|
|
261
262
|
} catch (error) {
|
|
262
|
-
console.error(
|
|
263
|
+
console.error(errorStyle(`Error deleting proxy: ${error.message}`));
|
|
263
264
|
return false;
|
|
264
265
|
}
|
|
265
266
|
}
|
|
@@ -273,7 +274,7 @@ async function openBrowser(url) {
|
|
|
273
274
|
await open(url, { app: [{ name: apps.browser }, 'firefox-developer-edition'] });
|
|
274
275
|
return true;
|
|
275
276
|
} catch (error) {
|
|
276
|
-
console.error(
|
|
277
|
+
console.error(errorStyle(`Error opening browser: ${error.message}`));
|
|
277
278
|
return false;
|
|
278
279
|
}
|
|
279
280
|
}
|
package/lib/resource.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { requireConfig } from './config.js';
|
|
3
2
|
import api from './api.js';
|
|
4
3
|
import { apiError } from './errors.js';
|
|
4
|
+
import { info, warning, hint } from './format.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* @return {Promise<Object>} Resource value
|
|
10
|
-
*/
|
|
6
|
+
// Drops internal resources whose keys start with `$`; those belong to
|
|
7
|
+
// the agent's own framework (cmd, fs, scripts…) and shouldn't surface
|
|
8
|
+
// through the user-facing resource list.
|
|
11
9
|
export async function getDeviceResources(deviceId) {
|
|
12
10
|
const config = requireConfig();
|
|
13
11
|
|
|
14
12
|
try {
|
|
13
|
+
// `/resources/api` merges firmware resources with API resources
|
|
14
|
+
// inherited from the product profile, which `/resources` alone omits.
|
|
15
15
|
const response = await api.get(
|
|
16
|
-
`/v3/users/${config.username}/devices/${deviceId}/resources`,
|
|
16
|
+
`/v3/users/${config.username}/devices/${deviceId}/resources/api`,
|
|
17
17
|
);
|
|
18
|
-
// Drop internal resources whose keys start with '$'
|
|
19
18
|
return Object.fromEntries(
|
|
20
19
|
Object.entries(response.data).filter(([key]) => !key.startsWith('$')),
|
|
21
20
|
);
|
|
@@ -66,9 +65,6 @@ export async function getDeviceResourceApi(deviceId, resourceId) {
|
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
/**
|
|
70
|
-
* Execute a resource on a device
|
|
71
|
-
*/
|
|
72
68
|
export async function executeDeviceResource(deviceId, resourceId, inputs) {
|
|
73
69
|
const config = requireConfig();
|
|
74
70
|
|
|
@@ -129,13 +125,13 @@ export async function listDeviceResourcesWithSchemas(deviceId) {
|
|
|
129
125
|
* other human-facing surface.
|
|
130
126
|
*/
|
|
131
127
|
export function formatDeviceResourcesWithSchemas(deviceId, entries) {
|
|
132
|
-
let out = `\nResources for device ${
|
|
128
|
+
let out = `\nResources for device ${info(deviceId)}:\n`;
|
|
133
129
|
if (!entries || entries.length === 0) {
|
|
134
|
-
return out +
|
|
130
|
+
return out + warning('No resources found for this device.\n');
|
|
135
131
|
}
|
|
136
132
|
for (const e of entries) {
|
|
137
133
|
const kind = getResourceType({ fn: e.fn });
|
|
138
|
-
out += `\n- ${
|
|
134
|
+
out += `\n- ${info(e.name)} ${hint('[' + kind + ']')}\n`;
|
|
139
135
|
if (e.in !== undefined) out += ` in: ${JSON.stringify(e.in)}\n`;
|
|
140
136
|
if (e.out !== undefined) out += ` out: ${JSON.stringify(e.out)}\n`;
|
|
141
137
|
}
|
package/lib/status.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { requireConfig } from './config.js';
|
|
3
2
|
import api from './api.js';
|
|
4
3
|
import { apiError } from './errors.js';
|
|
4
|
+
import {
|
|
5
|
+
formatUptime,
|
|
6
|
+
colorPct,
|
|
7
|
+
formatBytes,
|
|
8
|
+
label,
|
|
9
|
+
muted,
|
|
10
|
+
success,
|
|
11
|
+
error,
|
|
12
|
+
} from './format.js';
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
15
|
* Get device status
|
|
@@ -20,51 +28,84 @@ export async function getDeviceStatus(deviceId) {
|
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
|
-
* Format device status data for display
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
31
|
+
* Format device status data for display. When a `monitoring` payload
|
|
32
|
+
* (one sample from the monitoring bucket) is supplied the output
|
|
33
|
+
* includes a metrics section with CPU / memory / disk / load / uptime;
|
|
34
|
+
* otherwise the section is omitted.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} deviceId
|
|
37
|
+
* @param {Object} status
|
|
38
|
+
* @param {Object | null} [monitoring]
|
|
39
|
+
* @returns {string}
|
|
27
40
|
*/
|
|
28
|
-
export function formatDeviceStatus(deviceId, status) {
|
|
29
|
-
const connectedStatus = status.connected ?
|
|
41
|
+
export function formatDeviceStatus(deviceId, status, monitoring = null) {
|
|
42
|
+
const connectedStatus = status.connected ? success('● Online') : error('○ Offline');
|
|
30
43
|
|
|
31
|
-
// Format connected time
|
|
32
44
|
let connectedTime = '';
|
|
33
45
|
if (status.connected && status.connected_ts) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (diffSeconds < 60) {
|
|
39
|
-
connectedTime = `for ${diffSeconds} seconds`;
|
|
40
|
-
} else if (diffSeconds < 3600) {
|
|
41
|
-
connectedTime = `for ${Math.floor(diffSeconds / 60)} minutes`;
|
|
42
|
-
} else if (diffSeconds < 86400) {
|
|
43
|
-
connectedTime = `for ${Math.floor(diffSeconds / 3600)} hours`;
|
|
44
|
-
} else {
|
|
45
|
-
connectedTime = `for ${Math.floor(diffSeconds / 86400)} days`;
|
|
46
|
-
}
|
|
46
|
+
const diffSeconds = Math.floor(
|
|
47
|
+
(Date.now() - new Date(status.connected_ts).getTime()) / 1000,
|
|
48
|
+
);
|
|
49
|
+
connectedTime = `for ${formatUptime(diffSeconds)}`;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const lines = [''];
|
|
53
|
+
lines.push(label(`Device: ${deviceId}`));
|
|
54
|
+
lines.push(`Status: ${connectedStatus} ${connectedTime}`);
|
|
55
|
+
if (status.ip_address) lines.push(`IP: ${status.ip_address}`);
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Status: ${connectedStatus} ${connectedTime}
|
|
57
|
-
`;
|
|
57
|
+
if (monitoring) {
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push(label('Metrics:'));
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (monitoring.cpu?.usage != null) {
|
|
62
|
+
const bits = [];
|
|
63
|
+
if (monitoring.cpu.cores != null) bits.push(`${monitoring.cpu.cores} cores`);
|
|
64
|
+
if (monitoring.cpu.temperature != null)
|
|
65
|
+
bits.push(`${monitoring.cpu.temperature.toFixed(1)}°C`);
|
|
66
|
+
const detail = bits.length ? muted(` (${bits.join(', ')})`) : '';
|
|
67
|
+
lines.push(` CPU: ${colorPct(monitoring.cpu.usage)}${detail}`);
|
|
68
|
+
}
|
|
69
|
+
if (monitoring.memory?.usage != null) {
|
|
70
|
+
const total = monitoring.memory.total;
|
|
71
|
+
const avail = monitoring.memory.available;
|
|
72
|
+
const used = total != null && avail != null ? total - avail : null;
|
|
73
|
+
const detail =
|
|
74
|
+
used != null && total != null
|
|
75
|
+
? muted(` (${formatBytes(used)} of ${formatBytes(total)} used)`)
|
|
76
|
+
: '';
|
|
77
|
+
lines.push(` Memory: ${colorPct(monitoring.memory.usage)}${detail}`);
|
|
78
|
+
}
|
|
79
|
+
if (monitoring.disk?.root?.usage != null) {
|
|
80
|
+
const root = monitoring.disk.root;
|
|
81
|
+
const detail =
|
|
82
|
+
root.available != null && root.total != null
|
|
83
|
+
? muted(
|
|
84
|
+
` (${formatBytes(root.available)} free of ${formatBytes(root.total)})`,
|
|
85
|
+
)
|
|
86
|
+
: '';
|
|
87
|
+
lines.push(` Disk: ${colorPct(root.usage)}${detail}`);
|
|
88
|
+
}
|
|
89
|
+
if (monitoring.load) {
|
|
90
|
+
const l = monitoring.load;
|
|
91
|
+
const parts = [l['1m'], l['5m'], l['15m']].map((v) =>
|
|
92
|
+
v != null ? v.toFixed(2) : '—',
|
|
93
|
+
);
|
|
94
|
+
lines.push(` Load: ${parts.join(' · ')}`);
|
|
95
|
+
}
|
|
96
|
+
if (monitoring.uptime != null) {
|
|
97
|
+
lines.push(` Uptime: ${formatUptime(monitoring.uptime)}`);
|
|
98
|
+
}
|
|
99
|
+
if (monitoring.agent?.version) {
|
|
100
|
+
lines.push(` Agent: ${muted(monitoring.agent.version)}`);
|
|
101
|
+
}
|
|
61
102
|
}
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
Data Transfer:
|
|
65
|
-
↓ Received: ${
|
|
66
|
-
↑ Sent:
|
|
67
|
-
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(label('Data Transfer:'));
|
|
106
|
+
lines.push(` ↓ Received: ${formatBytes(status.rx_bytes || 0)}`);
|
|
107
|
+
lines.push(` ↑ Sent: ${formatBytes(status.tx_bytes || 0)}`);
|
|
108
|
+
lines.push('');
|
|
68
109
|
|
|
69
|
-
return
|
|
110
|
+
return lines.join('\n');
|
|
70
111
|
}
|