@thinremote/thinr-cli 1.0.2-alpha.2 → 1.1.0-alpha.1
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 +90 -0
- package/README.md +362 -87
- package/bin/thinr.js +51 -11
- package/commands/device/_shared.js +48 -0
- package/commands/device/console.js +15 -0
- package/commands/device/exec.js +76 -0
- package/commands/device/index.js +31 -0
- package/commands/device/list.js +43 -0
- package/commands/device/property.js +50 -0
- package/commands/device/proxy.js +42 -0
- package/commands/device/resource.js +51 -0
- package/commands/device/status.js +26 -0
- package/commands/device/update.js +53 -0
- package/commands/logout.js +12 -5
- package/commands/product.js +146 -177
- package/commands/profile.js +105 -0
- package/lib/api.js +54 -31
- package/lib/auth.js +225 -309
- package/lib/config.js +176 -45
- package/lib/console.js +87 -108
- package/lib/device-api.js +257 -0
- package/lib/devices.js +69 -0
- package/lib/errors.js +53 -0
- package/lib/mcp/helpers.js +34 -0
- package/lib/mcp/registry.js +32 -0
- package/lib/mcp/server.js +63 -0
- package/lib/mcp/tools-devices.js +194 -0
- package/lib/mcp/tools-exec.js +213 -0
- package/lib/mcp/tools-filesystem.js +163 -0
- package/lib/mcp/tools-products.js +232 -0
- package/lib/mcp/tools-profiles.js +30 -0
- package/lib/mcp/tools-properties.js +79 -0
- package/lib/mcp/tools-resources.js +76 -0
- package/lib/mcp/tools-scripts.js +172 -0
- package/lib/monitoring.js +72 -0
- package/lib/output.js +114 -0
- package/lib/product.js +373 -0
- package/lib/property.js +41 -57
- package/lib/proxy.js +58 -90
- package/lib/resource.js +106 -72
- package/lib/status.js +7 -33
- package/lib/storage.js +128 -0
- package/package.json +68 -37
- package/.github/workflows/publish.yml +0 -79
- package/commands/device.js +0 -268
- package/lib/device.js +0 -76
package/lib/proxy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import open, { apps } from 'open';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
-
import {
|
|
4
|
+
import { readConfig, requireConfig } from './config.js';
|
|
5
5
|
import api from './api.js';
|
|
6
|
+
import { apiError } from './errors.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Parse target argument into address and port
|
|
@@ -75,49 +76,43 @@ function parseTarget(target, defaultPort) {
|
|
|
75
76
|
return {
|
|
76
77
|
address: address || 'localhost',
|
|
77
78
|
port: port,
|
|
78
|
-
isSecure
|
|
79
|
+
isSecure,
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
*
|
|
84
|
+
* Open a proxy and keep it running until the user hits Ctrl+C.
|
|
85
|
+
* Resolves with `0` on a clean SIGINT shutdown, rejects with the
|
|
86
|
+
* underlying error if the proxy can't be created. Never calls
|
|
87
|
+
* process.exit — the wrapping CLI command decides what to do with
|
|
88
|
+
* the resolved code or the thrown error.
|
|
89
|
+
*
|
|
90
|
+
* @returns {Promise<number>} Exit code (0 on clean shutdown)
|
|
84
91
|
*/
|
|
85
92
|
export async function handleProxyAction(deviceId, protocol, target, options) {
|
|
86
|
-
|
|
87
|
-
if (!configExists()) {
|
|
88
|
-
console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Determine default port based on protocol
|
|
93
|
-
const defaultPort = protocol === 'http' ? 80 : (protocol === 'tls' ? 443 : 22);
|
|
93
|
+
const defaultPort = protocol === 'http' ? 80 : protocol === 'tls' ? 443 : 22;
|
|
94
94
|
const { address, port, isSecure } = parseTarget(target, defaultPort);
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
let proxyConfig = {
|
|
96
|
+
const proxyConfig = {
|
|
98
97
|
targetAddress: address,
|
|
99
98
|
targetPort: port,
|
|
100
99
|
serverPort: options.port ? parseInt(options.port) : null,
|
|
101
100
|
targetSecure: false,
|
|
102
101
|
serverSecure: false,
|
|
103
|
-
web: false
|
|
102
|
+
web: false,
|
|
104
103
|
};
|
|
105
104
|
|
|
106
105
|
switch (protocol) {
|
|
107
106
|
case 'tcp':
|
|
108
|
-
// TCP proxy without TLS
|
|
109
107
|
proxyConfig.serverSecure = false;
|
|
110
108
|
break;
|
|
111
109
|
case 'tls':
|
|
112
|
-
// TLS proxy
|
|
113
110
|
proxyConfig.targetSecure = true;
|
|
114
111
|
proxyConfig.serverSecure = true;
|
|
115
112
|
break;
|
|
116
113
|
case 'http':
|
|
117
|
-
// HTTP proxy
|
|
118
114
|
proxyConfig.web = true;
|
|
119
|
-
proxyConfig.serverSecure = true;
|
|
120
|
-
// Use the isSecure flag from parseTarget
|
|
115
|
+
proxyConfig.serverSecure = true;
|
|
121
116
|
proxyConfig.targetSecure = isSecure || port === 443;
|
|
122
117
|
break;
|
|
123
118
|
}
|
|
@@ -125,54 +120,44 @@ export async function handleProxyAction(deviceId, protocol, target, options) {
|
|
|
125
120
|
const proxyType = proxyConfig.web ? 'HTTP proxy' : `${protocol.toUpperCase()} proxy`;
|
|
126
121
|
const spinner = ora(`Creating ${proxyType} to ${deviceId} (${address}:${port})...`).start();
|
|
127
122
|
let proxyId = null;
|
|
123
|
+
let proxy;
|
|
128
124
|
|
|
129
125
|
try {
|
|
130
|
-
|
|
131
|
-
const proxy = await createProxy(deviceId, proxyConfig);
|
|
126
|
+
proxy = await createProxy(deviceId, proxyConfig);
|
|
132
127
|
proxyId = proxy.proxyId;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
spinner.fail(`Failed to create ${proxyType}`);
|
|
130
|
+
if (proxyId) await deleteProxy(proxyId);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (options.openBrowser !== false) {
|
|
143
|
-
await openBrowser(proxy.url);
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
// For TCP/TLS proxy, show address:port format
|
|
147
|
-
const secureInfo = isSecure ? ' [TLS]' : '';
|
|
148
|
-
spinner.succeed(`${protocol.toUpperCase()} proxy running on ${chalk.blue(proxy.serverHost + ':' + proxy.serverPort)} → ${deviceId} (${address}:${port})${secureInfo}`);
|
|
134
|
+
if (proxyConfig.web) {
|
|
135
|
+
const targetProtocol = isSecure ? 'https' : 'http';
|
|
136
|
+
const targetUrl = `${targetProtocol}://${address}${port === 80 || port === 443 ? '' : `:${port}`}`;
|
|
137
|
+
spinner.succeed(
|
|
138
|
+
`HTTP proxy running at ${chalk.blue(proxy.url)} → ${deviceId} -> ${chalk.cyan(targetUrl)}`,
|
|
139
|
+
);
|
|
140
|
+
if (options.openBrowser !== false) {
|
|
141
|
+
await openBrowser(proxy.url);
|
|
149
142
|
}
|
|
143
|
+
} else {
|
|
144
|
+
const secureInfo = isSecure ? ' [TLS]' : '';
|
|
145
|
+
spinner.succeed(
|
|
146
|
+
`${protocol.toUpperCase()} proxy running on ${chalk.blue(proxy.serverHost + ':' + proxy.serverPort)} → ${deviceId} (${address}:${port})${secureInfo}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
console.log(chalk.gray(`Press Ctrl+C to stop`));
|
|
151
|
+
process.stdin.resume();
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
// Resolve when the user hits Ctrl+C — the CLI command then exits cleanly.
|
|
154
|
+
return new Promise((resolve) => {
|
|
154
155
|
process.on('SIGINT', async () => {
|
|
155
156
|
console.log(chalk.yellow('\nStopping proxy...'));
|
|
156
|
-
if (proxyId)
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
process.exit(0);
|
|
157
|
+
if (proxyId) await deleteProxy(proxyId);
|
|
158
|
+
resolve(0);
|
|
160
159
|
});
|
|
161
|
-
|
|
162
|
-
// Keep the process running
|
|
163
|
-
process.stdin.resume();
|
|
164
|
-
|
|
165
|
-
} catch (error) {
|
|
166
|
-
spinner.fail(`Failed to create ${proxyType}`);
|
|
167
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
168
|
-
|
|
169
|
-
// Clean up if needed
|
|
170
|
-
if (proxyId) {
|
|
171
|
-
await deleteProxy(proxyId);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
160
|
+
});
|
|
176
161
|
}
|
|
177
162
|
|
|
178
163
|
/**
|
|
@@ -192,27 +177,22 @@ function getRandomPort(min = 50000, max = 51000) {
|
|
|
192
177
|
* @returns {Promise<Object>} Proxy information
|
|
193
178
|
*/
|
|
194
179
|
async function createProxy(deviceId, options) {
|
|
195
|
-
|
|
196
|
-
const config = readConfig();
|
|
197
|
-
|
|
198
|
-
if (!config.token || !config.server || !config.username) {
|
|
199
|
-
throw new Error('Not configured. Run thinr without parameters to set up.');
|
|
200
|
-
}
|
|
180
|
+
const config = requireConfig();
|
|
201
181
|
|
|
202
182
|
const protocol = options.web ? 'http_iotmp' : 'tcp_iotmp';
|
|
203
183
|
const targetAddress = options.targetAddress || 'localhost';
|
|
204
184
|
const targetPort = options.targetPort || (options.web ? 80 : 22);
|
|
205
185
|
const serverPort = options.serverPort || getRandomPort();
|
|
206
|
-
const serverSecure = options.serverSecure ||
|
|
186
|
+
const serverSecure = options.serverSecure || !!options.web;
|
|
207
187
|
const targetSecure = options.targetSecure || false;
|
|
208
188
|
|
|
209
189
|
// Create a unique proxy ID based on device, protocol and a timestamp
|
|
210
190
|
const timestamp = new Date().getTime().toString(36);
|
|
211
|
-
const proxyId = `${protocol}_${deviceId.slice(0,12)}_${timestamp}`;
|
|
191
|
+
const proxyId = `${protocol}_${deviceId.slice(0, 12)}_${timestamp}`;
|
|
212
192
|
|
|
213
193
|
try {
|
|
214
194
|
// Create proxy
|
|
215
|
-
|
|
195
|
+
await api.post(
|
|
216
196
|
`/v1/proxies`,
|
|
217
197
|
{
|
|
218
198
|
enabled: true,
|
|
@@ -223,23 +203,23 @@ async function createProxy(deviceId, options) {
|
|
|
223
203
|
device: deviceId,
|
|
224
204
|
address: targetAddress,
|
|
225
205
|
port: targetPort,
|
|
226
|
-
secure: targetSecure
|
|
206
|
+
secure: targetSecure,
|
|
227
207
|
},
|
|
228
208
|
protocol: protocol,
|
|
229
209
|
source: {
|
|
230
210
|
port: serverPort,
|
|
231
|
-
secure: serverSecure
|
|
232
|
-
}
|
|
211
|
+
secure: serverSecure,
|
|
212
|
+
},
|
|
233
213
|
},
|
|
234
214
|
proxy: proxyId,
|
|
235
215
|
name: options.web ? `Web access for ${deviceId}` : `TCP proxy for ${deviceId}`,
|
|
236
|
-
description: `${options.web ? 'Web interface' : 'TCP proxy'} for ${deviceId}:${targetPort} created by
|
|
216
|
+
description: `${options.web ? 'Web interface' : 'TCP proxy'} for ${deviceId}:${targetPort} created by Thinr CLI`,
|
|
237
217
|
},
|
|
238
218
|
{
|
|
239
219
|
headers: {
|
|
240
|
-
'Content-Type': 'application/json'
|
|
241
|
-
}
|
|
242
|
-
}
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
243
223
|
);
|
|
244
224
|
|
|
245
225
|
// Determine the URL based on protocol and SSL setting
|
|
@@ -255,20 +235,10 @@ async function createProxy(deviceId, options) {
|
|
|
255
235
|
targetPort: targetPort,
|
|
256
236
|
protocol: protocol,
|
|
257
237
|
serverSecure: serverSecure,
|
|
258
|
-
targetSecure: targetSecure
|
|
238
|
+
targetSecure: targetSecure,
|
|
259
239
|
};
|
|
260
240
|
} catch (error) {
|
|
261
|
-
|
|
262
|
-
if (error.response.status === 401) {
|
|
263
|
-
throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
|
|
264
|
-
} else {
|
|
265
|
-
throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
|
|
266
|
-
}
|
|
267
|
-
} else if (error.request) {
|
|
268
|
-
throw new Error('No response from server. Please check your connection.');
|
|
269
|
-
} else {
|
|
270
|
-
throw new Error(`Error: ${error.message}`);
|
|
271
|
-
}
|
|
241
|
+
throw apiError(error, { notFound: `Device not found: ${deviceId}` });
|
|
272
242
|
}
|
|
273
243
|
}
|
|
274
244
|
|
|
@@ -285,9 +255,7 @@ async function deleteProxy(proxyId) {
|
|
|
285
255
|
}
|
|
286
256
|
|
|
287
257
|
try {
|
|
288
|
-
await api.delete(
|
|
289
|
-
`/v1/proxies/${proxyId}`,
|
|
290
|
-
);
|
|
258
|
+
await api.delete(`/v1/proxies/${proxyId}`);
|
|
291
259
|
|
|
292
260
|
return true;
|
|
293
261
|
} catch (error) {
|
|
@@ -302,10 +270,10 @@ async function deleteProxy(proxyId) {
|
|
|
302
270
|
*/
|
|
303
271
|
async function openBrowser(url) {
|
|
304
272
|
try {
|
|
305
|
-
await open(url, {app: [{name: apps.browser}, 'firefox-developer-edition']});
|
|
273
|
+
await open(url, { app: [{ name: apps.browser }, 'firefox-developer-edition'] });
|
|
306
274
|
return true;
|
|
307
275
|
} catch (error) {
|
|
308
276
|
console.error(chalk.red(`Error opening browser: ${error.message}`));
|
|
309
277
|
return false;
|
|
310
278
|
}
|
|
311
|
-
}
|
|
279
|
+
}
|
package/lib/resource.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { requireConfig } from './config.js';
|
|
3
3
|
import api from './api.js';
|
|
4
|
+
import { apiError } from './errors.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Get device resource value
|
|
@@ -8,116 +9,149 @@ import api from './api.js';
|
|
|
8
9
|
* @return {Promise<Object>} Resource value
|
|
9
10
|
*/
|
|
10
11
|
export async function getDeviceResources(deviceId) {
|
|
11
|
-
|
|
12
|
-
const config = readConfig();
|
|
13
|
-
|
|
14
|
-
if (!config.token || !config.server || !config.username) {
|
|
15
|
-
throw new Error('Not configured. Run thinr without parameters to set up.');
|
|
16
|
-
}
|
|
12
|
+
const config = requireConfig();
|
|
17
13
|
|
|
18
14
|
try {
|
|
19
|
-
// Fetch resource value
|
|
20
15
|
const response = await api.get(
|
|
21
16
|
`/v3/users/${config.username}/devices/${deviceId}/resources`,
|
|
22
17
|
);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Object.entries(response.data).filter(([key]) => !key.startsWith('$'))
|
|
18
|
+
// Drop internal resources whose keys start with '$'
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.entries(response.data).filter(([key]) => !key.startsWith('$')),
|
|
27
21
|
);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw apiError(error, { notFound: `Device not found: ${deviceId}` });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Read the current value of a resource via GET. Appropriate for
|
|
29
|
+
* output-only (fn=3) or fn=4 resources when you only want the latest
|
|
30
|
+
* sample, not to trigger the input side.
|
|
31
|
+
*/
|
|
32
|
+
export async function readDeviceResource(deviceId, resourceId) {
|
|
33
|
+
const config = requireConfig();
|
|
34
|
+
try {
|
|
35
|
+
const response = await api.get(
|
|
36
|
+
`/v3/users/${config.username}/devices/${deviceId}/resources/${resourceId}`,
|
|
37
|
+
);
|
|
38
|
+
return response.data;
|
|
30
39
|
} catch (error) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new Error(`Device not found: ${deviceId}`);
|
|
35
|
-
} else if (error.response.status === 401) {
|
|
36
|
-
throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
|
|
37
|
-
} else {
|
|
38
|
-
throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
|
|
39
|
-
}
|
|
40
|
-
} else if (error.request) {
|
|
41
|
-
// The request was made but no response was received
|
|
42
|
-
throw new Error('No response from server. Please check your connection.');
|
|
43
|
-
} else {
|
|
44
|
-
// Something happened in setting up the request that triggered an Error
|
|
45
|
-
throw new Error(`Error: ${error.message}`);
|
|
46
|
-
}
|
|
40
|
+
throw apiError(error, {
|
|
41
|
+
notFound: `Resource not found: ${resourceId} on device ${deviceId}`,
|
|
42
|
+
});
|
|
47
43
|
}
|
|
44
|
+
}
|
|
48
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Introspect a single resource. Returns the `{in, out}` schema the
|
|
48
|
+
* agent advertises for that resource, e.g. for `cmd`:
|
|
49
|
+
* { in: { cmd: "", mode: "api", timeout: 30 }, out: { stdout: "", stderr: "", retcode: 0 } }
|
|
50
|
+
*
|
|
51
|
+
* Read-only resources (fn=3) only return `out`; input-only (fn=2) only
|
|
52
|
+
* return `in`; fn=1 may return an empty object. Not every agent/firmware
|
|
53
|
+
* exposes /api — callers should tolerate 404/501.
|
|
54
|
+
*/
|
|
55
|
+
export async function getDeviceResourceApi(deviceId, resourceId) {
|
|
56
|
+
const config = requireConfig();
|
|
57
|
+
try {
|
|
58
|
+
const response = await api.get(
|
|
59
|
+
`/v3/users/${config.username}/devices/${deviceId}/resources/${resourceId}/api`,
|
|
60
|
+
);
|
|
61
|
+
return response.data;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw apiError(error, {
|
|
64
|
+
notFound: `Resource not found: ${resourceId} on device ${deviceId}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
/**
|
|
52
70
|
* Execute a resource on a device
|
|
53
71
|
*/
|
|
54
72
|
export async function executeDeviceResource(deviceId, resourceId, inputs) {
|
|
55
|
-
|
|
56
|
-
const config = readConfig();
|
|
57
|
-
|
|
58
|
-
if (!config.token || !config.server || !config.username) {
|
|
59
|
-
throw new Error('Not configured. Run thinr without parameters to set up.');
|
|
60
|
-
}
|
|
73
|
+
const config = requireConfig();
|
|
61
74
|
|
|
62
75
|
try {
|
|
63
|
-
// Execute resource
|
|
64
76
|
const response = await api.post(
|
|
65
77
|
`/v3/users/${config.username}/devices/${deviceId}/resources/${resourceId}`,
|
|
66
78
|
inputs,
|
|
67
79
|
);
|
|
68
|
-
|
|
69
80
|
return response.data;
|
|
70
81
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
throw new Error(`Resource not found: ${resourceId} on device ${deviceId}`);
|
|
75
|
-
} else if (error.response.status === 401) {
|
|
76
|
-
throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
|
|
77
|
-
} else {
|
|
78
|
-
throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
|
|
79
|
-
}
|
|
80
|
-
} else if (error.request) {
|
|
81
|
-
// The request was made but no response was received
|
|
82
|
-
throw new Error('No response from server. Please check your connection.');
|
|
83
|
-
} else {
|
|
84
|
-
// Something happened in setting up the request that triggered an Error
|
|
85
|
-
throw new Error(`Error: ${error.message}`);
|
|
86
|
-
}
|
|
82
|
+
throw apiError(error, {
|
|
83
|
+
notFound: `Resource not found: ${resourceId} on device ${deviceId}`,
|
|
84
|
+
});
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
89
|
+
* Decide GET vs POST based on whether the caller passed real inputs.
|
|
90
|
+
* No inputs (undefined, null, or an empty object) → GET, which reads
|
|
91
|
+
* the output side of fn=3/4 resources without triggering the input
|
|
92
|
+
* handler. Otherwise POST with the payload.
|
|
93
|
+
*/
|
|
94
|
+
export async function callDeviceResource(deviceId, resourceId, inputs) {
|
|
95
|
+
const hasInputs =
|
|
96
|
+
inputs !== undefined &&
|
|
97
|
+
inputs !== null &&
|
|
98
|
+
!(typeof inputs === 'object' && !Array.isArray(inputs) && Object.keys(inputs).length === 0);
|
|
99
|
+
return hasInputs
|
|
100
|
+
? executeDeviceResource(deviceId, resourceId, inputs)
|
|
101
|
+
: readDeviceResource(deviceId, resourceId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch the resource list and, in parallel, introspect each resource's
|
|
106
|
+
* `/api` schema. Returns an array with `{name, fn, in?, out?}` so both
|
|
107
|
+
* the CLI and the MCP can render the same information.
|
|
95
108
|
*/
|
|
96
|
-
export function
|
|
97
|
-
|
|
109
|
+
export async function listDeviceResourcesWithSchemas(deviceId) {
|
|
110
|
+
const resources = await getDeviceResources(deviceId);
|
|
111
|
+
const names = Object.keys(resources);
|
|
112
|
+
const entries = await Promise.all(
|
|
113
|
+
names.map(async (name) => {
|
|
114
|
+
const fn = resources[name]?.fn;
|
|
115
|
+
try {
|
|
116
|
+
const schema = await getDeviceResourceApi(deviceId, name);
|
|
117
|
+
return { name, fn, ...schema };
|
|
118
|
+
} catch {
|
|
119
|
+
return { name, fn };
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
98
125
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Format the resource entries produced by listDeviceResourcesWithSchemas
|
|
128
|
+
* (`{name, fn, in?, out?}`). Shared between CLI (with chalk) and any
|
|
129
|
+
* other human-facing surface.
|
|
130
|
+
*/
|
|
131
|
+
export function formatDeviceResourcesWithSchemas(deviceId, entries) {
|
|
132
|
+
let out = `\nResources for device ${chalk.cyan(deviceId)}:\n`;
|
|
133
|
+
if (!entries || entries.length === 0) {
|
|
134
|
+
return out + chalk.yellow('No resources found for this device.\n');
|
|
106
135
|
}
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
const kind = getResourceType({ fn: e.fn });
|
|
138
|
+
out += `\n- ${chalk.green(e.name)} ${chalk.gray('[' + kind + ']')}\n`;
|
|
139
|
+
if (e.in !== undefined) out += ` in: ${JSON.stringify(e.in)}\n`;
|
|
140
|
+
if (e.out !== undefined) out += ` out: ${JSON.stringify(e.out)}\n`;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
109
143
|
}
|
|
110
144
|
|
|
111
145
|
function getResourceType(value) {
|
|
112
146
|
switch (value.fn) {
|
|
113
147
|
case 1:
|
|
114
|
-
return
|
|
148
|
+
return 'no parameters';
|
|
115
149
|
case 2:
|
|
116
|
-
return
|
|
150
|
+
return 'input';
|
|
117
151
|
case 3:
|
|
118
|
-
return
|
|
152
|
+
return 'output';
|
|
119
153
|
case 4:
|
|
120
|
-
return
|
|
154
|
+
return 'input/output';
|
|
121
155
|
default:
|
|
122
156
|
return 'unknown';
|
|
123
157
|
}
|
package/lib/status.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { requireConfig } from './config.js';
|
|
3
3
|
import api from './api.js';
|
|
4
|
+
import { apiError } from './errors.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Get device status
|
|
@@ -8,38 +9,13 @@ import api from './api.js';
|
|
|
8
9
|
* @returns {Promise<Object>} Status information
|
|
9
10
|
*/
|
|
10
11
|
export async function getDeviceStatus(deviceId) {
|
|
11
|
-
|
|
12
|
-
const config = readConfig();
|
|
13
|
-
|
|
14
|
-
if (!config.token || !config.server || !config.username) {
|
|
15
|
-
throw new Error('Not configured. Run thinr without parameters to set up.');
|
|
16
|
-
}
|
|
12
|
+
const config = requireConfig();
|
|
17
13
|
|
|
18
14
|
try {
|
|
19
|
-
|
|
20
|
-
const response = await api.get(
|
|
21
|
-
`/v1/users/${config.username}/devices/${deviceId}/stats`,
|
|
22
|
-
);
|
|
23
|
-
|
|
15
|
+
const response = await api.get(`/v1/users/${config.username}/devices/${deviceId}/stats`);
|
|
24
16
|
return response.data;
|
|
25
17
|
} catch (error) {
|
|
26
|
-
|
|
27
|
-
// The request was made and the server responded with a status code
|
|
28
|
-
// that falls out of the range of 2xx
|
|
29
|
-
if (error.response.status === 404) {
|
|
30
|
-
throw new Error(`Device not found: ${deviceId}`);
|
|
31
|
-
} else if (error.response.status === 401) {
|
|
32
|
-
throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
|
|
33
|
-
} else {
|
|
34
|
-
throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
|
|
35
|
-
}
|
|
36
|
-
} else if (error.request) {
|
|
37
|
-
// The request was made but no response was received
|
|
38
|
-
throw new Error('No response from server. Please check your connection.');
|
|
39
|
-
} else {
|
|
40
|
-
// Something happened in setting up the request that triggered an Error
|
|
41
|
-
throw new Error(`Error: ${error.message}`);
|
|
42
|
-
}
|
|
18
|
+
throw apiError(error, { notFound: `Device not found: ${deviceId}` });
|
|
43
19
|
}
|
|
44
20
|
}
|
|
45
21
|
|
|
@@ -50,9 +26,7 @@ export async function getDeviceStatus(deviceId) {
|
|
|
50
26
|
* @returns {string} Formatted status
|
|
51
27
|
*/
|
|
52
28
|
export function formatDeviceStatus(deviceId, status) {
|
|
53
|
-
const connectedStatus = status.connected ?
|
|
54
|
-
chalk.green('● Online') :
|
|
55
|
-
chalk.red('○ Offline');
|
|
29
|
+
const connectedStatus = status.connected ? chalk.green('● Online') : chalk.red('○ Offline');
|
|
56
30
|
|
|
57
31
|
// Format connected time
|
|
58
32
|
let connectedTime = '';
|
|
@@ -93,4 +67,4 @@ Data Transfer:
|
|
|
93
67
|
`;
|
|
94
68
|
|
|
95
69
|
return output;
|
|
96
|
-
}
|
|
70
|
+
}
|