@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { deleteConfig, configExists } from '../lib/config.js';
|
|
4
|
+
import { deleteToken } from '../lib/auth.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the logout command
|
|
8
|
+
* @param {Command} program - Commander program instance
|
|
9
|
+
*/
|
|
10
|
+
export function logoutCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('logout')
|
|
13
|
+
.description('Remove stored credentials and configuration')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
// Check if configured
|
|
16
|
+
if (!configExists()) {
|
|
17
|
+
console.log(chalk.yellow('No configuration found. Already logged out.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spinner = ora('Removing configuration...').start();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Delete token from server first
|
|
25
|
+
spinner.text = 'Deleting token from server...';
|
|
26
|
+
await deleteToken();
|
|
27
|
+
|
|
28
|
+
// Delete local configuration
|
|
29
|
+
spinner.text = 'Removing local configuration...';
|
|
30
|
+
const success = deleteConfig();
|
|
31
|
+
|
|
32
|
+
if (success) {
|
|
33
|
+
spinner.succeed('Logged out successfully. Configuration removed.');
|
|
34
|
+
} else {
|
|
35
|
+
spinner.fail('Failed to remove configuration.');
|
|
36
|
+
console.error(chalk.red('Error: Unable to delete configuration file. You may need to remove it manually.'));
|
|
37
|
+
console.error(chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'));
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
spinner.fail('Failed to remove configuration.');
|
|
41
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
42
|
+
console.error(chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { configExists } from "../lib/config.js";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { formatDeviceProperty, getDeviceProperty } from "../lib/property.js";
|
|
5
|
+
import { executeDeviceResource } from "../lib/resource.js";
|
|
6
|
+
import { filterActiveDevices, getDevices } from "../lib/device.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register the product command with all product related functionality.
|
|
10
|
+
* @param {Command} program - Commander program instance
|
|
11
|
+
*/
|
|
12
|
+
export function productCommand(program) {
|
|
13
|
+
|
|
14
|
+
const product = program.command('product').description('Product commands');
|
|
15
|
+
|
|
16
|
+
product
|
|
17
|
+
.argument('<productId>', 'ID of the product to manage')
|
|
18
|
+
.description('Device commands for a specific product')
|
|
19
|
+
.allowUnknownOption(true)
|
|
20
|
+
.allowExcessArguments(true)
|
|
21
|
+
.action( async (productId, options, command) => {
|
|
22
|
+
|
|
23
|
+
// Get additional arguments manually
|
|
24
|
+
const args = command.args.slice(1); // Skip productId
|
|
25
|
+
const action = args[0];
|
|
26
|
+
const target = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
27
|
+
|
|
28
|
+
// Check if configured
|
|
29
|
+
if (!configExists()) {
|
|
30
|
+
console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If no action provided, show help
|
|
35
|
+
if (!action) {
|
|
36
|
+
printHelp(productId);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse options manually from process.argv
|
|
41
|
+
const parsedOptions = {
|
|
42
|
+
json: false,
|
|
43
|
+
inputs: {},
|
|
44
|
+
field: "",
|
|
45
|
+
allDevices: false,
|
|
46
|
+
group: null // Group option for filtering devices
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const argv = process.argv;
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
if (argv[i] === '-j' || argv[i] === '--json') {
|
|
52
|
+
parsedOptions.json = true;
|
|
53
|
+
} else if (argv[i] === '-i' || argv[i] === '--input') {
|
|
54
|
+
let inputPair = argv[i + 1].split('=');
|
|
55
|
+
parsedOptions.inputs[inputPair[0]] = inputPair[1];
|
|
56
|
+
} else if (argv[i] === '-f' || argv[i] === '--field') {
|
|
57
|
+
parsedOptions.field = argv[i + 1];
|
|
58
|
+
} else if (argv[i] === '-h' || argv[i] === '--help') {
|
|
59
|
+
printHelp(productId);
|
|
60
|
+
return;
|
|
61
|
+
} else if (argv[i] === '-a' || argv[i] === '--all') {
|
|
62
|
+
parsedOptions.allDevices = true;
|
|
63
|
+
} else if (argv[i] === '-g' || argv[i] === '--group') {
|
|
64
|
+
parsedOptions.group = argv[i + 1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let devices = [];
|
|
70
|
+
const spinner = ora(`Retrieving devices for product ${productId}...`).start();
|
|
71
|
+
try {
|
|
72
|
+
let filter = {
|
|
73
|
+
productId: productId,
|
|
74
|
+
};
|
|
75
|
+
if (parsedOptions.group) {
|
|
76
|
+
filter.asset_group = parsedOptions.group;
|
|
77
|
+
}
|
|
78
|
+
devices = await getDevices(filter);
|
|
79
|
+
spinner.succeed(`Devices for product ${productId} retrieved successfully`);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
spinner.fail(`Failed to retrieve devices for product ${productId}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
switch (action.toLowerCase()) {
|
|
86
|
+
|
|
87
|
+
case 'property': {
|
|
88
|
+
|
|
89
|
+
//const spinner = ora(`Retrieving property ${target} for devices belonging to the product ${productId}...`).start();
|
|
90
|
+
|
|
91
|
+
if ( ! parsedOptions.allDevices ) {
|
|
92
|
+
devices = filterActiveDevices(devices);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
for (const device of devices) {
|
|
97
|
+
try {
|
|
98
|
+
const property = await getDeviceProperty(device.device, target);
|
|
99
|
+
console.log('Device', chalk.blue(device.device), 'property', chalk.blue(target));
|
|
100
|
+
|
|
101
|
+
if (parsedOptions.json) {
|
|
102
|
+
if ( parsedOptions.field ) {
|
|
103
|
+
console.log(parsedOptions.field.split('.').reduce((obj, key) => obj && obj[key], property));
|
|
104
|
+
} else {
|
|
105
|
+
console.log(JSON.stringify(property, null, 2));
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
console.log(formatDeviceProperty(device.device, target, property));
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error(chalk.red(`Error retrieving property ${target} for device ${device.device}: ${e.message}`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//spinner.succeed(`Property ${target} for devices belonging to product ${productId} found`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
spinner.fail(`Property retrieval failed`);
|
|
117
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'resource': {
|
|
124
|
+
|
|
125
|
+
//const spinner = ora(`Executing resource ${target} for devices belonging to the product ${productId}...`).start();
|
|
126
|
+
|
|
127
|
+
// Obviously, we can only execute resources on active devices
|
|
128
|
+
devices = filterActiveDevices(devices);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
for (const device of devices) {
|
|
132
|
+
try {
|
|
133
|
+
const result = await executeDeviceResource(device.device, target, parsedOptions.inputs);
|
|
134
|
+
console.log('Device', chalk.blue(device.device), 'resource', chalk.blue(target));
|
|
135
|
+
if ( parsedOptions.field ) {
|
|
136
|
+
console.log(parsedOptions.field.split('.').reduce((obj, key) => obj && obj[key], result));
|
|
137
|
+
} else {
|
|
138
|
+
console.log(result);
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(chalk.red(`Error retrieving property ${target} for device ${device.device}: ${e.message}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//spinner.succeed(`Resource ${target} executed successfully for devices belonging to product ${productId}`);
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
spinner.fail(`Resource execution failed`);
|
|
149
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
default:
|
|
158
|
+
console.error(chalk.red(`Error: Unknown action '${action}'. Available actions: property, resource.\n`));
|
|
159
|
+
printHelp(productId);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printHelp(productId) {
|
|
167
|
+
console.log(`Usage: thinr device product ${productId} <command> [options]
|
|
168
|
+
|
|
169
|
+
Available commands for device product "${productId}":
|
|
170
|
+
|
|
171
|
+
property [propertyId] Get a specific property of all devices belonging to the product
|
|
172
|
+
Example: thinr device product ${productId} property uptime
|
|
173
|
+
|
|
174
|
+
resource [resource] Execute a specific resource of all devices belonging to the product
|
|
175
|
+
Example: thinr device product ${productId} resource reboot
|
|
176
|
+
|
|
177
|
+
Arguments:
|
|
178
|
+
|
|
179
|
+
propertyId ID of the property to retrieve (for property command)
|
|
180
|
+
|
|
181
|
+
resource Name of the resource to execute (for resource command)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
Options (for all product subcommands):
|
|
185
|
+
-g, --group <group> Group of devices to filter (e.g., -g group1)
|
|
186
|
+
|
|
187
|
+
Options (for property command):
|
|
188
|
+
-j, --json Output as JSON
|
|
189
|
+
-f, --field <field> Field to extract from the property (e.g., -f data.value)
|
|
190
|
+
-a, --all Include non active devices (default: false)
|
|
191
|
+
|
|
192
|
+
Options (for resource command):
|
|
193
|
+
-j, --json Output as JSON
|
|
194
|
+
-f, --field <field> Field to extract from the property (e.g., -f data.value)
|
|
195
|
+
|
|
196
|
+
Use "thinr product device ${productId} <command> --help" for more details on each command.`);
|
|
197
|
+
|
|
198
|
+
}
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { refreshToken, getAccessToken } from './auth.js'; // Adjust the import based on your project structure
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
|
|
6
|
+
const httpAgent = new http.Agent({ keepAlive: true });
|
|
7
|
+
const httpsAgent = new https.Agent({ keepAlive: true });
|
|
8
|
+
|
|
9
|
+
const api = axios.create({
|
|
10
|
+
// You can leave baseURL undefined initially
|
|
11
|
+
timeout: 10000,
|
|
12
|
+
httpAgent,
|
|
13
|
+
httpsAgent,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function setBaseURL(url) {
|
|
17
|
+
api.defaults.baseURL = url;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
api.interceptors.request.use(request => {
|
|
21
|
+
|
|
22
|
+
const token = getAccessToken();
|
|
23
|
+
if (token) {
|
|
24
|
+
request.headers['Authorization'] = `Bearer ${token}`;
|
|
25
|
+
}
|
|
26
|
+
return request;
|
|
27
|
+
}, error => {
|
|
28
|
+
return Promise.reject(error);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
api.interceptors.response.use(
|
|
32
|
+
response => response,
|
|
33
|
+
async error => {
|
|
34
|
+
const originalRequest = error.config;
|
|
35
|
+
|
|
36
|
+
// Only handle 401 once
|
|
37
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
38
|
+
originalRequest._retry = true;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const newToken = await refreshToken();
|
|
42
|
+
|
|
43
|
+
// Set new token on headers
|
|
44
|
+
api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
|
|
45
|
+
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
|
|
46
|
+
|
|
47
|
+
// return the retried request
|
|
48
|
+
return api(originalRequest);
|
|
49
|
+
} catch (refreshError) {
|
|
50
|
+
// Token refresh failed, reject to trigger outer catch
|
|
51
|
+
return Promise.reject(refreshError);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Any other errors
|
|
56
|
+
return Promise.reject(error);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export default api;
|