@thinremote/thinr-cli 1.0.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/.github/workflows/publish.yml +79 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +22 -0
- package/README.md +206 -0
- package/bin/thinr.js +78 -0
- package/commands/device.js +272 -0
- package/commands/logout.js +45 -0
- package/commands/product.js +198 -0
- package/lib/api.js +60 -0
- package/lib/auth.js +463 -0
- package/lib/config.js +88 -0
- package/lib/console.js +172 -0
- package/lib/device.js +76 -0
- package/lib/property.js +134 -0
- package/lib/proxy.js +310 -0
- package/lib/resource.js +124 -0
- package/lib/status.js +96 -0
- package/package.json +39 -0
package/lib/resource.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readConfig } from './config.js';
|
|
3
|
+
import api from './api.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get device resource value
|
|
7
|
+
* @param {string} deviceId - Device ID to query
|
|
8
|
+
* @return {Promise<Object>} Resource value
|
|
9
|
+
*/
|
|
10
|
+
export async function getDeviceResources(deviceId) {
|
|
11
|
+
// Get configuration
|
|
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
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Fetch resource value
|
|
20
|
+
const response = await api.get(
|
|
21
|
+
`/v3/users/${config.username}/devices/${deviceId}/resources`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Remove hidden resources for which the keys starting with '_'
|
|
25
|
+
const filtered = Object.fromEntries(
|
|
26
|
+
Object.entries(response.data).filter(([key]) => !key.startsWith('$'))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return filtered;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.response) {
|
|
32
|
+
// The request was made and the server responded with a status code
|
|
33
|
+
if (error.response.status === 404) {
|
|
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
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute a resource on a device
|
|
53
|
+
*/
|
|
54
|
+
export async function executeDeviceResource(deviceId, resourceId, inputs) {
|
|
55
|
+
// Get configuration
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Execute resource
|
|
64
|
+
const response = await api.post(
|
|
65
|
+
`/v3/users/${config.username}/devices/${deviceId}/resources/${resourceId}`,
|
|
66
|
+
inputs,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return response.data;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.response) {
|
|
72
|
+
// The request was made and the server responded with a status code
|
|
73
|
+
if (error.response.status === 404) {
|
|
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
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format device resources for display
|
|
92
|
+
* @param {string} deviceId - Device ID
|
|
93
|
+
* @param {Object} resources - Resources data from API
|
|
94
|
+
* @return {string} Formatted resources
|
|
95
|
+
*/
|
|
96
|
+
export function formatDeviceResources(deviceId, resources) {
|
|
97
|
+
let formattedResources = `\nResources for device ${chalk.cyan(deviceId)}:\n`;
|
|
98
|
+
|
|
99
|
+
//
|
|
100
|
+
if (resources && Object.keys(resources).length > 0) {
|
|
101
|
+
Object.keys(resources).forEach(key => {
|
|
102
|
+
formattedResources += `- ${chalk.green(key)}: ${chalk.blue(getResourceType(resources[key]))}\n`;
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
formattedResources += chalk.yellow('No resources found for this device.\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return formattedResources;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getResourceType(value) {
|
|
112
|
+
switch (value.fn) {
|
|
113
|
+
case 1:
|
|
114
|
+
return "no parameters";
|
|
115
|
+
case 2:
|
|
116
|
+
return "input";
|
|
117
|
+
case 3:
|
|
118
|
+
return "output";
|
|
119
|
+
case 4:
|
|
120
|
+
return "input/output";
|
|
121
|
+
default:
|
|
122
|
+
return 'unknown';
|
|
123
|
+
}
|
|
124
|
+
}
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readConfig } from './config.js';
|
|
3
|
+
import api from './api.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get device status
|
|
7
|
+
* @param {string} deviceId - Device ID to check
|
|
8
|
+
* @returns {Promise<Object>} Status information
|
|
9
|
+
*/
|
|
10
|
+
export async function getDeviceStatus(deviceId) {
|
|
11
|
+
// Get configuration
|
|
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
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Fetch device status
|
|
20
|
+
const response = await api.get(
|
|
21
|
+
`/v1/users/${config.username}/devices/${deviceId}/stats`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return response.data;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.response) {
|
|
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format device status data for display
|
|
48
|
+
* @param {string} deviceId - Device ID
|
|
49
|
+
* @param {Object} status - Status data from API
|
|
50
|
+
* @returns {string} Formatted status
|
|
51
|
+
*/
|
|
52
|
+
export function formatDeviceStatus(deviceId, status) {
|
|
53
|
+
const connectedStatus = status.connected ?
|
|
54
|
+
chalk.green('● Online') :
|
|
55
|
+
chalk.red('○ Offline');
|
|
56
|
+
|
|
57
|
+
// Format connected time
|
|
58
|
+
let connectedTime = '';
|
|
59
|
+
if (status.connected && status.connected_ts) {
|
|
60
|
+
const now = new Date().getTime();
|
|
61
|
+
const connectedSince = new Date(status.connected_ts).getTime();
|
|
62
|
+
const diffSeconds = Math.floor((now - connectedSince) / 1000);
|
|
63
|
+
|
|
64
|
+
if (diffSeconds < 60) {
|
|
65
|
+
connectedTime = `for ${diffSeconds} seconds`;
|
|
66
|
+
} else if (diffSeconds < 3600) {
|
|
67
|
+
connectedTime = `for ${Math.floor(diffSeconds / 60)} minutes`;
|
|
68
|
+
} else if (diffSeconds < 86400) {
|
|
69
|
+
connectedTime = `for ${Math.floor(diffSeconds / 3600)} hours`;
|
|
70
|
+
} else {
|
|
71
|
+
connectedTime = `for ${Math.floor(diffSeconds / 86400)} days`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Format data transfer
|
|
76
|
+
const rxMB = status.rx_bytes ? (status.rx_bytes / (1024 * 1024)).toFixed(2) : '0.00';
|
|
77
|
+
const txMB = status.tx_bytes ? (status.tx_bytes / (1024 * 1024)).toFixed(2) : '0.00';
|
|
78
|
+
|
|
79
|
+
// Build the output
|
|
80
|
+
let output = `
|
|
81
|
+
${chalk.bold(`Device: ${deviceId}`)}
|
|
82
|
+
Status: ${connectedStatus} ${connectedTime}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
if (status.ip_address) {
|
|
86
|
+
output += `IP Address: ${status.ip_address}\n`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
output += `
|
|
90
|
+
Data Transfer:
|
|
91
|
+
↓ Received: ${rxMB} MB
|
|
92
|
+
↑ Sent: ${txMB} MB
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
return output;
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thinremote/thinr-cli",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "CLI for ThinRemote - Remote management for IoT devices",
|
|
5
|
+
"main": "bin/thinr.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"thinr": "bin/thinr.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/thinr.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"iot",
|
|
16
|
+
"remote",
|
|
17
|
+
"management",
|
|
18
|
+
"thing",
|
|
19
|
+
"thinger"
|
|
20
|
+
],
|
|
21
|
+
"author": "ThinRemote",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"axios": "^1.10.0",
|
|
25
|
+
"chalk": "^5.4.1",
|
|
26
|
+
"commander": "^14.0.0",
|
|
27
|
+
"conf": "^14.0.0",
|
|
28
|
+
"figlet": "^1.8.1",
|
|
29
|
+
"@inquirer/prompts": "^7.5.3",
|
|
30
|
+
"jsonwebtoken": "^9.0.2",
|
|
31
|
+
"open": "^10.1.2",
|
|
32
|
+
"ora": "^8.2.0",
|
|
33
|
+
"ws": "^8.18.2"
|
|
34
|
+
},
|
|
35
|
+
"type": "module",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.16"
|
|
38
|
+
}
|
|
39
|
+
}
|