@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +192 -16
  3. package/bin/thinr.js +23 -24
  4. package/commands/_shared.js +154 -0
  5. package/commands/dashboard.js +17 -0
  6. package/commands/device/_shared.js +17 -34
  7. package/commands/device/console.js +1 -0
  8. package/commands/device/delete.js +53 -0
  9. package/commands/device/exec.js +7 -6
  10. package/commands/device/fs.js +193 -0
  11. package/commands/device/index.js +9 -0
  12. package/commands/device/list.js +35 -23
  13. package/commands/device/logs.js +178 -0
  14. package/commands/device/property.js +30 -28
  15. package/commands/device/proxy.js +3 -0
  16. package/commands/device/resource.js +30 -29
  17. package/commands/device/status.js +35 -14
  18. package/commands/device/transfer.js +236 -0
  19. package/commands/device/update.js +15 -10
  20. package/commands/fleet/index.js +17 -0
  21. package/commands/fleet/upgrade.js +275 -0
  22. package/commands/logout.js +10 -11
  23. package/commands/playbook/_shared.js +12 -0
  24. package/commands/playbook/index.js +18 -0
  25. package/commands/playbook/run.js +302 -0
  26. package/commands/playbook/validate.js +38 -0
  27. package/commands/product/_shared.js +8 -0
  28. package/commands/product/exec.js +243 -0
  29. package/commands/product/fs.js +218 -0
  30. package/commands/product/index.js +35 -0
  31. package/commands/product/list.js +86 -0
  32. package/commands/product/logs.js +289 -0
  33. package/commands/product/metric.js +124 -0
  34. package/commands/product/playbook.js +911 -0
  35. package/commands/product/property.js +76 -0
  36. package/commands/product/props.js +125 -0
  37. package/commands/product/push.js +184 -0
  38. package/commands/product/resource.js +76 -0
  39. package/commands/product/status.js +213 -0
  40. package/commands/profile.js +76 -8
  41. package/dist/dashboard.js +5644 -0
  42. package/lib/agent-releases.js +59 -0
  43. package/lib/agent-versions.js +73 -0
  44. package/lib/alarms.js +252 -0
  45. package/lib/api.js +8 -2
  46. package/lib/auth.js +30 -17
  47. package/lib/bucket.js +87 -0
  48. package/lib/command-stream.js +199 -0
  49. package/lib/config.js +5 -3
  50. package/lib/console.js +12 -4
  51. package/lib/constants.js +76 -0
  52. package/lib/dashboard/event-stream.js +239 -0
  53. package/lib/dashboard/metric-aggregate.js +64 -0
  54. package/lib/dashboard/metric-filter.js +39 -0
  55. package/lib/debug-log.js +93 -0
  56. package/lib/device-api.js +109 -147
  57. package/lib/devices.js +18 -21
  58. package/lib/fleet-upgrade.js +106 -0
  59. package/lib/format.js +114 -0
  60. package/lib/mcp/registry.js +14 -0
  61. package/lib/mcp/tools-alarms.js +510 -0
  62. package/lib/mcp/tools-buckets.js +103 -0
  63. package/lib/mcp/tools-devices.js +33 -3
  64. package/lib/mcp/tools-exec.js +30 -17
  65. package/lib/mcp/tools-filesystem.js +102 -6
  66. package/lib/mcp/tools-install.js +153 -0
  67. package/lib/mcp/tools-logs.js +225 -0
  68. package/lib/mcp/tools-playbook.js +211 -0
  69. package/lib/mcp/tools-products.js +1226 -105
  70. package/lib/mcp/tools-profile.js +634 -0
  71. package/lib/mcp/tools-tokens.js +371 -0
  72. package/lib/monitoring.js +26 -1
  73. package/lib/playbook/actions.js +447 -0
  74. package/lib/playbook/expression.js +302 -0
  75. package/lib/playbook/fleet.js +175 -0
  76. package/lib/playbook/loader.js +247 -0
  77. package/lib/playbook/runner.js +300 -0
  78. package/lib/playbook/schema.js +209 -0
  79. package/lib/playbook/vars.js +209 -0
  80. package/lib/product/log-presets.js +196 -0
  81. package/lib/product/logs.js +485 -0
  82. package/lib/product-orchestrator.js +124 -0
  83. package/lib/product.js +523 -20
  84. package/lib/profile.js +665 -0
  85. package/lib/property.js +7 -40
  86. package/lib/proxy.js +9 -8
  87. package/lib/resource.js +10 -14
  88. package/lib/status.js +78 -37
  89. package/lib/tokens.js +374 -0
  90. package/package.json +14 -3
  91. 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
- * Create or update a property value on a device. Uses PUT so the
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 ' + chalk.cyan(deviceId) + ':\n';
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
- chalk.cyan(propertyId) +
63
+ info(propertyId) +
97
64
  ' for device ' +
98
- chalk.cyan(deviceId) +
65
+ info(deviceId) +
99
66
  ':\n';
100
67
  const flattenedValue = flatten(propertyValue);
101
68
  for (const key in flattenedValue) {
102
- formattedValue += `${key}: ${chalk.green(flattenedValue[key])}\n`;
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 ${chalk.blue(proxy.url)} → ${deviceId} -> ${chalk.cyan(targetUrl)}`,
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 ${chalk.blue(proxy.serverHost + ':' + proxy.serverPort)} → ${deviceId} (${address}:${port})${secureInfo}`,
147
+ `${protocol.toUpperCase()} proxy running on ${info(proxy.serverHost + ':' + proxy.serverPort)} → ${deviceId} (${address}:${port})${secureInfo}`,
147
148
  );
148
149
  }
149
150
 
150
- console.log(chalk.gray(`Press Ctrl+C to stop`));
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(chalk.yellow('\nStopping proxy...'));
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 = 50000, max = 51000) {
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(chalk.red(`Error deleting proxy: ${error.message}`));
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(chalk.red(`Error opening browser: ${error.message}`));
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
- * Get device resource value
8
- * @param {string} deviceId - Device ID to query
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 ${chalk.cyan(deviceId)}:\n`;
128
+ let out = `\nResources for device ${info(deviceId)}:\n`;
133
129
  if (!entries || entries.length === 0) {
134
- return out + chalk.yellow('No resources found for this device.\n');
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- ${chalk.green(e.name)} ${chalk.gray('[' + kind + ']')}\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
- * @param {string} deviceId - Device ID
25
- * @param {Object} status - Status data from API
26
- * @returns {string} Formatted status
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 ? chalk.green('● Online') : chalk.red('○ Offline');
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 now = new Date().getTime();
35
- const connectedSince = new Date(status.connected_ts).getTime();
36
- const diffSeconds = Math.floor((now - connectedSince) / 1000);
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
- // Format data transfer
50
- const rxMB = status.rx_bytes ? (status.rx_bytes / (1024 * 1024)).toFixed(2) : '0.00';
51
- const txMB = status.tx_bytes ? (status.tx_bytes / (1024 * 1024)).toFixed(2) : '0.00';
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
- // Build the output
54
- let output = `
55
- ${chalk.bold(`Device: ${deviceId}`)}
56
- Status: ${connectedStatus} ${connectedTime}
57
- `;
57
+ if (monitoring) {
58
+ lines.push('');
59
+ lines.push(label('Metrics:'));
58
60
 
59
- if (status.ip_address) {
60
- output += `IP Address: ${status.ip_address}\n`;
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
- output += `
64
- Data Transfer:
65
- ↓ Received: ${rxMB} MB
66
- ↑ Sent: ${txMB} MB
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 output;
110
+ return lines.join('\n');
70
111
  }