@thinremote/thinr-cli 1.0.2 → 1.1.0-alpha.2
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 +67 -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/config.js
CHANGED
|
@@ -3,14 +3,39 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Profile-based configuration store.
|
|
7
|
+
*
|
|
8
|
+
* On disk the config file looks like:
|
|
9
|
+
* {
|
|
10
|
+
* "default": "<profile-name>",
|
|
11
|
+
* "profiles": {
|
|
12
|
+
* "<profile-name>": { server, username, token, refresh_token, ... }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Profile names default to the server hostname so login against a new
|
|
17
|
+
* server automatically creates an isolated profile.
|
|
18
|
+
*
|
|
19
|
+
* Legacy single-profile files (top-level server/username/token) are
|
|
20
|
+
* detected on read and migrated to the new layout transparently. The
|
|
21
|
+
* migration uses the legacy `server` field as the profile name.
|
|
22
|
+
*
|
|
23
|
+
* The "active" profile is the one all helpers operate on. It is picked
|
|
24
|
+
* (in order): explicit override via setActiveProfile(), the THINR_PROFILE
|
|
25
|
+
* environment variable, the file's `default` field.
|
|
8
26
|
*/
|
|
9
|
-
|
|
27
|
+
|
|
28
|
+
let activeProfileOverride = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the on-disk config path, creating the parent directory on
|
|
32
|
+
* first access. Internal — every consumer goes through readStore /
|
|
33
|
+
* writeStore so they never have to know the location.
|
|
34
|
+
*/
|
|
35
|
+
function getConfigPath() {
|
|
10
36
|
const homeDir = os.homedir();
|
|
11
37
|
const configDir = path.join(homeDir, '.config', 'thinr-cli');
|
|
12
38
|
|
|
13
|
-
// Create config directory if it doesn't exist
|
|
14
39
|
if (!fs.existsSync(configDir)) {
|
|
15
40
|
fs.mkdirSync(configDir, { recursive: true });
|
|
16
41
|
}
|
|
@@ -18,71 +43,177 @@ export function getConfigPath() {
|
|
|
18
43
|
return path.join(configDir, 'config.json');
|
|
19
44
|
}
|
|
20
45
|
|
|
46
|
+
function emptyStore() {
|
|
47
|
+
return { default: null, profiles: {} };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readStore() {
|
|
51
|
+
const configPath = getConfigPath();
|
|
52
|
+
if (!fs.existsSync(configPath)) return emptyStore();
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return emptyStore();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Legacy format: top-level {server, username, token, ...}. Migrate to
|
|
61
|
+
// a profile keyed by the server hostname.
|
|
62
|
+
if (raw && typeof raw === 'object' && !raw.profiles && raw.server) {
|
|
63
|
+
const name = String(raw.server);
|
|
64
|
+
return { default: name, profiles: { [name]: { ...raw } } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!raw || typeof raw !== 'object' || !raw.profiles) return emptyStore();
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeStore(store) {
|
|
72
|
+
const configPath = getConfigPath();
|
|
73
|
+
fs.writeFileSync(configPath, JSON.stringify(store, null, 2), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
21
76
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
77
|
+
* Override the active profile for the current process. Pass null to fall
|
|
78
|
+
* back to env var / file default. Used by the CLI entry point when the
|
|
79
|
+
* user passes --profile.
|
|
24
80
|
*/
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
return fs.existsSync(configPath);
|
|
81
|
+
export function setActiveProfile(name) {
|
|
82
|
+
activeProfileOverride = name || null;
|
|
28
83
|
}
|
|
29
84
|
|
|
30
85
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @returns {Object} Configuration object
|
|
86
|
+
* Resolve the active profile name, or null if no profile exists.
|
|
33
87
|
*/
|
|
34
|
-
export function
|
|
35
|
-
|
|
88
|
+
export function getActiveProfile() {
|
|
89
|
+
if (activeProfileOverride) return activeProfileOverride;
|
|
90
|
+
if (process.env.THINR_PROFILE) return process.env.THINR_PROFILE;
|
|
91
|
+
const store = readStore();
|
|
92
|
+
if (store.default && store.profiles[store.default]) return store.default;
|
|
93
|
+
// Fall back to the only profile if there's exactly one.
|
|
94
|
+
const names = Object.keys(store.profiles);
|
|
95
|
+
return names.length === 1 ? names[0] : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List all profile names.
|
|
100
|
+
*/
|
|
101
|
+
export function listProfiles() {
|
|
102
|
+
return Object.keys(readStore().profiles);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get a specific profile by name (or undefined if missing).
|
|
107
|
+
*/
|
|
108
|
+
export function getProfile(name) {
|
|
109
|
+
const store = readStore();
|
|
110
|
+
return store.profiles[name];
|
|
111
|
+
}
|
|
36
112
|
|
|
37
|
-
|
|
38
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Set the persisted default profile.
|
|
115
|
+
*/
|
|
116
|
+
export function setDefaultProfile(name) {
|
|
117
|
+
const store = readStore();
|
|
118
|
+
if (!store.profiles[name]) {
|
|
119
|
+
throw new Error(`Profile not found: ${name}`);
|
|
39
120
|
}
|
|
121
|
+
store.default = name;
|
|
122
|
+
writeStore(store);
|
|
123
|
+
}
|
|
40
124
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Remove a profile by name. Returns true if removed.
|
|
127
|
+
*/
|
|
128
|
+
export function deleteProfile(name) {
|
|
129
|
+
const store = readStore();
|
|
130
|
+
if (!store.profiles[name]) return false;
|
|
131
|
+
delete store.profiles[name];
|
|
132
|
+
if (store.default === name) {
|
|
133
|
+
const remaining = Object.keys(store.profiles);
|
|
134
|
+
store.default = remaining[0] || null;
|
|
47
135
|
}
|
|
136
|
+
writeStore(store);
|
|
137
|
+
return true;
|
|
48
138
|
}
|
|
49
139
|
|
|
50
140
|
/**
|
|
51
|
-
*
|
|
52
|
-
* @param {Object} config - Configuration object
|
|
141
|
+
* Check if any profile is configured.
|
|
53
142
|
*/
|
|
54
|
-
export function
|
|
55
|
-
const
|
|
56
|
-
|
|
143
|
+
export function configExists() {
|
|
144
|
+
const store = readStore();
|
|
145
|
+
if (!Object.keys(store.profiles).length) return false;
|
|
146
|
+
return getActiveProfile() != null;
|
|
147
|
+
}
|
|
57
148
|
|
|
58
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Read the active profile's data.
|
|
151
|
+
* @returns {Object} Active profile fields (server, username, token, ...) or {}
|
|
152
|
+
*/
|
|
153
|
+
export function readConfig() {
|
|
154
|
+
const store = readStore();
|
|
155
|
+
const name = getActiveProfile();
|
|
156
|
+
if (!name) return {};
|
|
157
|
+
return store.profiles[name] || {};
|
|
59
158
|
}
|
|
60
159
|
|
|
61
160
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
161
|
+
* Read the active profile and assert that it has the fields every API
|
|
162
|
+
* call needs (server, username, token). Throws a tagged error so the
|
|
163
|
+
* MCP and CLI surfaces both classify the failure as `not_configured`
|
|
164
|
+
* via classifyError. Replaces the same boilerplate that used to live
|
|
165
|
+
* at the top of every helper in lib/.
|
|
166
|
+
*/
|
|
167
|
+
export function requireConfig() {
|
|
168
|
+
const config = readConfig();
|
|
169
|
+
if (!config.token || !config.server || !config.username) {
|
|
170
|
+
const err = new Error('Not configured. Run thinr without parameters to set up.');
|
|
171
|
+
err.code = 'not_configured';
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
return config;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Write a fresh profile from the supplied data. The profile name is
|
|
179
|
+
* derived from the supplied `server`, or from the active profile name
|
|
180
|
+
* if no server is provided. Becomes the default if there isn't one yet.
|
|
181
|
+
*/
|
|
182
|
+
export function writeConfig(config) {
|
|
183
|
+
const store = readStore();
|
|
184
|
+
const name = (config && config.server) || getActiveProfile();
|
|
185
|
+
if (!name) {
|
|
186
|
+
throw new Error('Cannot write config without a server / profile name');
|
|
187
|
+
}
|
|
188
|
+
store.profiles[name] = { ...config };
|
|
189
|
+
if (!store.default) store.default = name;
|
|
190
|
+
writeStore(store);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Merge fields into the active profile.
|
|
64
195
|
*/
|
|
65
196
|
export function mergeConfig(newConfig) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
197
|
+
const store = readStore();
|
|
198
|
+
const name = getActiveProfile();
|
|
199
|
+
if (!name) {
|
|
200
|
+
// No active profile yet: fall back to writeConfig semantics.
|
|
201
|
+
return writeConfig(newConfig);
|
|
202
|
+
}
|
|
203
|
+
store.profiles[name] = { ...store.profiles[name], ...newConfig };
|
|
204
|
+
writeStore(store);
|
|
69
205
|
}
|
|
70
206
|
|
|
71
207
|
/**
|
|
72
|
-
* Delete
|
|
73
|
-
* @returns {boolean} True if deleted successfully
|
|
208
|
+
* Delete the entire config (all profiles).
|
|
74
209
|
*/
|
|
75
210
|
export function deleteConfig() {
|
|
76
211
|
const configPath = getConfigPath();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
212
|
+
if (!fs.existsSync(configPath)) return true;
|
|
213
|
+
try {
|
|
214
|
+
fs.unlinkSync(configPath);
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
85
218
|
}
|
|
86
|
-
|
|
87
|
-
return true; // Already doesn't exist
|
|
88
|
-
}
|
|
219
|
+
}
|
package/lib/console.js
CHANGED
|
@@ -1,134 +1,116 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
3
|
+
import { requireConfig } from './config.js';
|
|
4
4
|
import api from './api.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Creates a WebSocket connection to a device terminal
|
|
7
|
+
* Creates a WebSocket connection to a device terminal. Returns a
|
|
8
|
+
* promise that resolves with `0` when the remote side closes
|
|
9
|
+
* cleanly, and rejects with a tagged Error when the connection
|
|
10
|
+
* fails (404 → not_found, 401/403 → unauthorized, anything else →
|
|
11
|
+
* error). The CLI command wrapping this call decides what to do
|
|
12
|
+
* with the resolved code or the thrown error — the helper itself
|
|
13
|
+
* never calls process.exit.
|
|
14
|
+
*
|
|
8
15
|
* @param {string} deviceId - The ID of the device to connect to
|
|
16
|
+
* @returns {Promise<number>} Exit code (0 on clean close)
|
|
9
17
|
*/
|
|
10
18
|
export async function connectToDeviceConsole(deviceId) {
|
|
11
|
-
|
|
12
|
-
const config = readConfig();
|
|
19
|
+
const config = requireConfig();
|
|
13
20
|
|
|
14
|
-
if (!config.token || !config.server || !config.username) {
|
|
15
|
-
console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Create a unique session ID
|
|
20
21
|
const sessionId = createUUID();
|
|
21
|
-
|
|
22
|
-
// Create WebSocket URL
|
|
23
22
|
const wsUrl = `wss://${config.server}/v3/users/${config.username}/devices/${deviceId}/resources/$terminal/${sessionId}?raw=1`;
|
|
24
|
-
|
|
25
|
-
//console.log(chalk.blue(`Connecting to ${deviceId}...`));
|
|
26
|
-
|
|
27
|
-
// Create WebSocket connection
|
|
28
23
|
const ws = new WebSocket(wsUrl, {
|
|
29
|
-
headers: {
|
|
30
|
-
'Authorization': `Bearer ${config.token}`
|
|
31
|
-
}
|
|
24
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
32
25
|
});
|
|
33
26
|
|
|
34
|
-
// Flag to track if terminal settings have been restored
|
|
35
27
|
let terminalRestored = false;
|
|
36
|
-
|
|
37
|
-
// Setup raw mode for process.stdin
|
|
38
28
|
process.stdin.setRawMode(true);
|
|
39
29
|
process.stdin.resume();
|
|
40
30
|
process.stdin.setEncoding('utf8');
|
|
41
31
|
|
|
42
|
-
// Function to restore terminal settings
|
|
43
32
|
const restoreTerminal = () => {
|
|
44
33
|
if (!terminalRestored) {
|
|
45
|
-
// Restore terminal settings
|
|
46
34
|
process.stdin.setRawMode(false);
|
|
47
35
|
process.stdin.pause();
|
|
48
36
|
terminalRestored = true;
|
|
49
37
|
}
|
|
50
38
|
};
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
ws.on('open', () => {
|
|
42
|
+
updateTerminalSize(
|
|
43
|
+
config,
|
|
44
|
+
deviceId,
|
|
45
|
+
sessionId,
|
|
46
|
+
process.stdout.columns,
|
|
47
|
+
process.stdout.rows,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
process.stdout.on('resize', () => {
|
|
51
|
+
updateTerminalSize(
|
|
52
|
+
config,
|
|
53
|
+
deviceId,
|
|
54
|
+
sessionId,
|
|
55
|
+
process.stdout.columns,
|
|
56
|
+
process.stdout.rows,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
process.stdin.on('data', (data) => {
|
|
61
|
+
// Ctrl+D (EOT) to exit gracefully
|
|
62
|
+
if (data.toString() === '\u0004') {
|
|
63
|
+
ws.close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Forward everything else (including Ctrl+C) to the device
|
|
67
|
+
ws.send(data);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ws.on('message', (data) => {
|
|
71
|
+
if (!(data instanceof Buffer)) data = Buffer.from(data);
|
|
72
|
+
process.stdout.write(data);
|
|
73
|
+
});
|
|
62
74
|
});
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Forward any other input (including Ctrl+C) to the remote device
|
|
73
|
-
ws.send(data);
|
|
76
|
+
ws.on('close', () => {
|
|
77
|
+
console.log(chalk.yellow('\nConnection closed'));
|
|
78
|
+
restoreTerminal();
|
|
79
|
+
// Small delay so any final output flushes before the caller exits.
|
|
80
|
+
setTimeout(() => resolve(0), 100);
|
|
74
81
|
});
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
ws.on('error', (error) => {
|
|
84
|
+
restoreTerminal();
|
|
85
|
+
const m = /(\d{3})/.exec(error.message || '');
|
|
86
|
+
const status = m ? parseInt(m[1], 10) : 0;
|
|
87
|
+
let msg, code;
|
|
88
|
+
if (status === 404) {
|
|
89
|
+
msg = `Device "${deviceId}" not found or offline.`;
|
|
90
|
+
code = 'not_found';
|
|
91
|
+
} else if (status === 401 || status === 403) {
|
|
92
|
+
msg = `Not authorized to access "${deviceId}". Check your token or user.`;
|
|
93
|
+
code = 'unauthorized';
|
|
94
|
+
} else {
|
|
95
|
+
msg = `Connection error: ${error.message}`;
|
|
96
|
+
code = 'error';
|
|
81
97
|
}
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
const err = new Error(msg);
|
|
99
|
+
err.code = code;
|
|
100
|
+
reject(err);
|
|
84
101
|
});
|
|
85
|
-
});
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Small delay before exiting to flush any remaining output
|
|
93
|
-
setTimeout(() => {
|
|
94
|
-
process.exit(0);
|
|
95
|
-
}, 100);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Handle WebSocket errors
|
|
99
|
-
ws.on('error', (error) => {
|
|
100
|
-
console.error(chalk.red(`\nWebSocket error: ${error.message}`));
|
|
101
|
-
restoreTerminal();
|
|
102
|
-
process.exit(1);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Handle SIGINT (Ctrl+C) at process level
|
|
106
|
-
process.on('SIGINT', () => {
|
|
107
|
-
// Try to send Ctrl+C to the remote terminal
|
|
108
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
109
|
-
ws.send('\u0003');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Don't exit our process - let the remote terminal handle it
|
|
113
|
-
return false;
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Ensure cleanup on other exit signals
|
|
117
|
-
process.on('SIGTERM', () => {
|
|
118
|
-
restoreTerminal();
|
|
119
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
120
|
-
ws.close();
|
|
121
|
-
}
|
|
122
|
-
});
|
|
103
|
+
// Forward Ctrl+C to the remote terminal instead of killing ourselves.
|
|
104
|
+
process.on('SIGINT', () => {
|
|
105
|
+
if (ws.readyState === WebSocket.OPEN) ws.send('\u0003');
|
|
106
|
+
return false;
|
|
107
|
+
});
|
|
123
108
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ws.close();
|
|
130
|
-
}
|
|
131
|
-
process.exit(1);
|
|
109
|
+
// Cleanup on shutdown signals (no exit — the WS close will resolve us).
|
|
110
|
+
process.on('SIGTERM', () => {
|
|
111
|
+
restoreTerminal();
|
|
112
|
+
if (ws.readyState === WebSocket.OPEN) ws.close();
|
|
113
|
+
});
|
|
132
114
|
});
|
|
133
115
|
}
|
|
134
116
|
|
|
@@ -144,15 +126,12 @@ async function updateTerminalSize(config, deviceId, sessionId, cols, rows) {
|
|
|
144
126
|
try {
|
|
145
127
|
const url = `/v3/users/${config.username}/devices/${deviceId}/resources/$terminal/${sessionId}/params`;
|
|
146
128
|
|
|
147
|
-
await api.post(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
cols: cols,
|
|
152
|
-
rows: rows
|
|
153
|
-
}
|
|
129
|
+
await api.post(url, {
|
|
130
|
+
size: {
|
|
131
|
+
cols: cols,
|
|
132
|
+
rows: rows,
|
|
154
133
|
},
|
|
155
|
-
);
|
|
134
|
+
});
|
|
156
135
|
} catch (error) {
|
|
157
136
|
console.error(chalk.red(`Error updating terminal size: ${error.message}`));
|
|
158
137
|
}
|
|
@@ -164,9 +143,9 @@ async function updateTerminalSize(config, deviceId, sessionId, cols, rows) {
|
|
|
164
143
|
*/
|
|
165
144
|
function createUUID() {
|
|
166
145
|
let dt = new Date().getTime();
|
|
167
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
168
|
-
const r = (dt + Math.random() * 16) % 16 | 0;
|
|
146
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
147
|
+
const r = ((dt + Math.random() * 16) % 16) | 0;
|
|
169
148
|
dt = Math.floor(dt / 16);
|
|
170
|
-
return (c === 'x' ? r : (r & 0x3 | 0x8)
|
|
149
|
+
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
171
150
|
});
|
|
172
|
-
}
|
|
151
|
+
}
|