@thinremote/thinr-cli 1.0.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +362 -87
  3. package/bin/thinr.js +51 -11
  4. package/commands/device/_shared.js +48 -0
  5. package/commands/device/console.js +15 -0
  6. package/commands/device/exec.js +76 -0
  7. package/commands/device/index.js +31 -0
  8. package/commands/device/list.js +43 -0
  9. package/commands/device/property.js +50 -0
  10. package/commands/device/proxy.js +42 -0
  11. package/commands/device/resource.js +51 -0
  12. package/commands/device/status.js +26 -0
  13. package/commands/device/update.js +53 -0
  14. package/commands/logout.js +12 -5
  15. package/commands/product.js +146 -177
  16. package/commands/profile.js +105 -0
  17. package/lib/api.js +54 -31
  18. package/lib/auth.js +225 -309
  19. package/lib/config.js +176 -45
  20. package/lib/console.js +87 -108
  21. package/lib/device-api.js +257 -0
  22. package/lib/devices.js +69 -0
  23. package/lib/errors.js +53 -0
  24. package/lib/mcp/helpers.js +34 -0
  25. package/lib/mcp/registry.js +32 -0
  26. package/lib/mcp/server.js +63 -0
  27. package/lib/mcp/tools-devices.js +194 -0
  28. package/lib/mcp/tools-exec.js +213 -0
  29. package/lib/mcp/tools-filesystem.js +163 -0
  30. package/lib/mcp/tools-products.js +232 -0
  31. package/lib/mcp/tools-profiles.js +30 -0
  32. package/lib/mcp/tools-properties.js +79 -0
  33. package/lib/mcp/tools-resources.js +76 -0
  34. package/lib/mcp/tools-scripts.js +172 -0
  35. package/lib/monitoring.js +72 -0
  36. package/lib/output.js +114 -0
  37. package/lib/product.js +373 -0
  38. package/lib/property.js +41 -57
  39. package/lib/proxy.js +58 -90
  40. package/lib/resource.js +106 -72
  41. package/lib/status.js +7 -33
  42. package/lib/storage.js +128 -0
  43. package/package.json +68 -37
  44. package/.github/workflows/publish.yml +0 -79
  45. package/commands/device.js +0 -268
  46. package/lib/device.js +0 -76
package/lib/output.js ADDED
@@ -0,0 +1,114 @@
1
+ import ora from 'ora';
2
+
3
+ let jsonMode = false;
4
+
5
+ export function setJsonMode(on) {
6
+ jsonMode = !!on;
7
+ }
8
+
9
+ export function isJsonMode() {
10
+ return jsonMode;
11
+ }
12
+
13
+ /**
14
+ * Detect --json / -j in process.argv as early as possible so every
15
+ * subcommand can rely on the mode being set before it runs its own
16
+ * parser. Mirrors the early --profile scan in bin/thinr.js.
17
+ */
18
+ export function detectJsonModeFromArgv(argv = process.argv) {
19
+ for (let i = 2; i < argv.length; i++) {
20
+ const a = argv[i];
21
+ if (a === '--json' || a === '-j') return setJsonMode(true);
22
+ if (a === '--') return;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Print a successful result. In JSON mode, writes a single line with the
28
+ * canonical envelope to stdout. Otherwise the caller is expected to have
29
+ * already printed a human-readable rendering.
30
+ */
31
+ export function printOk(data) {
32
+ if (!jsonMode) return;
33
+ process.stdout.write(JSON.stringify({ ok: true, data: data ?? null }, null, 2) + '\n');
34
+ }
35
+
36
+ /**
37
+ * Print an error envelope and exit. In JSON mode writes the envelope to
38
+ * stdout so scripts can pipe it to jq; in human mode writes a red error
39
+ * line to stderr. Always exits with the given code (default 1).
40
+ */
41
+ export function printErr(message, { code = 'error', exitCode = 1 } = {}) {
42
+ if (jsonMode) {
43
+ process.stdout.write(
44
+ JSON.stringify({ ok: false, error: { message: String(message), code } }, null, 2) +
45
+ '\n',
46
+ );
47
+ } else {
48
+ process.stderr.write(`Error [${code}]: ${message}\n`);
49
+ }
50
+ process.exit(exitCode);
51
+ }
52
+
53
+ /**
54
+ * Create a spinner that becomes a no-op when JSON mode is active. Keeps
55
+ * call-sites symmetrical: spinner.start(), spinner.succeed(), spinner.fail()
56
+ * all work whether JSON mode is on or off.
57
+ */
58
+ export function createSpinner(text) {
59
+ if (jsonMode) {
60
+ return {
61
+ start() {
62
+ return this;
63
+ },
64
+ stop() {
65
+ return this;
66
+ },
67
+ succeed() {
68
+ return this;
69
+ },
70
+ fail() {
71
+ return this;
72
+ },
73
+ set text(_) {},
74
+ get text() {
75
+ return '';
76
+ },
77
+ };
78
+ }
79
+ return ora(text);
80
+ }
81
+
82
+ /**
83
+ * Map an error from api.js / axios into a stable { message, code } pair.
84
+ * Centralized here so every command reports the same codes in JSON mode.
85
+ */
86
+ export function classifyError(error) {
87
+ // Preferred path: library functions use apiError() in lib/errors.js to
88
+ // attach a .code before throwing. Fall back to the raw-axios mapping
89
+ // only when something throws without going through that helper.
90
+ if (error && typeof error.code === 'string' && error.message) {
91
+ return { code: error.code, message: error.message };
92
+ }
93
+ if (error && error.response) {
94
+ const status = error.response.status;
95
+ if (status === 401 || status === 403)
96
+ return {
97
+ code: 'unauthorized',
98
+ message: 'Unauthorized. Your token may have expired. Please reconfigure.',
99
+ };
100
+ if (status === 404)
101
+ return { code: 'not_found', message: error.message || 'Resource not found' };
102
+ return {
103
+ code: 'server_error',
104
+ message: `Server error: ${status} ${error.response.statusText || ''}`.trim(),
105
+ };
106
+ }
107
+ if (error && error.request) {
108
+ return {
109
+ code: 'network_error',
110
+ message: 'No response from server. Please check your connection.',
111
+ };
112
+ }
113
+ return { code: 'error', message: error?.message || String(error) };
114
+ }
package/lib/product.js ADDED
@@ -0,0 +1,373 @@
1
+ import api from './api.js';
2
+ import { apiError, inputError } from './errors.js';
3
+ import { requireConfig } from './config.js';
4
+ import {
5
+ storageExists,
6
+ createStorage,
7
+ deleteStorage,
8
+ listStorageFiles,
9
+ readStorageFile,
10
+ storageFileExists,
11
+ writeStorageFile,
12
+ deleteStorageFile,
13
+ } from './storage.js';
14
+
15
+ /**
16
+ * Helpers for ThinRemote products and the product-script workflow.
17
+ *
18
+ * Low-level helpers (`getProducts`, `getProduct`, `createProduct`, …)
19
+ * each map to a single REST endpoint and throw `apiError` on failure
20
+ * — except `*Exists` (boolean) and `delete*` (true if removed, false
21
+ * if the resource was already absent).
22
+ *
23
+ * Higher-level orchestrators (`installProductScript`,
24
+ * `removeProductScript`, `deleteProductWithStorage`) compose those
25
+ * primitives and return a `{ steps: string[] }` log so callers can
26
+ * surface a human-readable summary without re-implementing the
27
+ * sequence.
28
+ */
29
+
30
+ function resolveUser(user) {
31
+ return user || requireConfig().username;
32
+ }
33
+
34
+ function v1(user) {
35
+ return `/v1/users/${resolveUser(user)}`;
36
+ }
37
+
38
+ // ─── Low-level: products ─────────────────────────────────────────────
39
+
40
+ export async function getProducts(user) {
41
+ try {
42
+ const res = await api.get(`${v1(user)}/products`);
43
+ return res.data || [];
44
+ } catch (e) {
45
+ throw apiError(e);
46
+ }
47
+ }
48
+
49
+ export async function getProduct(productId, user) {
50
+ try {
51
+ const res = await api.get(`${v1(user)}/products/${productId}`);
52
+ return res.data || {};
53
+ } catch (e) {
54
+ throw apiError(e, { notFound: `Product not found: ${productId}` });
55
+ }
56
+ }
57
+
58
+ export async function productExists(productId, user) {
59
+ try {
60
+ await api.get(`${v1(user)}/products/${productId}`);
61
+ return true;
62
+ } catch (e) {
63
+ if (e.response?.status === 404) return false;
64
+ throw apiError(e);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Create a product with the standard ThinRemote scaffold: `type:
70
+ * thinremote`, a Font Awesome icon (defaults to Linux), and a
71
+ * `monitoring` bucket fed every 60 s from the device's `monitoring`
72
+ * resource. Caller controls only `icon`, `name`, `description`.
73
+ */
74
+ export async function createProduct(
75
+ productId,
76
+ user,
77
+ { icon = 'fab fa-linux', name, description } = {},
78
+ ) {
79
+ try {
80
+ await api.post(`${v1(user)}/products`, {
81
+ product: productId,
82
+ name: name || productId,
83
+ description: description || 'ThinRemote product',
84
+ enabled: true,
85
+ config: {
86
+ type: 'thinremote',
87
+ icons: [
88
+ {
89
+ conditions: [],
90
+ icon: { type: 'fa', source: icon, color: '#000000', background: '#ffffff' },
91
+ },
92
+ ],
93
+ },
94
+ profile: {
95
+ buckets: {
96
+ monitoring: {
97
+ name: 'Monitoring Data',
98
+ description: 'ThinRemote device metrics',
99
+ enabled: true,
100
+ backend: 'mongodb',
101
+ data: {
102
+ source: 'resource',
103
+ resource: 'monitoring',
104
+ update: 'interval',
105
+ interval: 60,
106
+ magnitude: 'second',
107
+ payload: '{{payload}}',
108
+ payload_function: '',
109
+ payload_type: 'source_payload',
110
+ },
111
+ retention: { period: 3, unit: 'months' },
112
+ tags: [],
113
+ },
114
+ },
115
+ },
116
+ });
117
+ } catch (e) {
118
+ throw apiError(e);
119
+ }
120
+ }
121
+
122
+ export async function updateProduct(productId, user, patch) {
123
+ try {
124
+ await api.put(`${v1(user)}/products/${productId}`, patch);
125
+ } catch (e) {
126
+ throw apiError(e, { notFound: `Product not found: ${productId}` });
127
+ }
128
+ }
129
+
130
+ export async function deleteProduct(productId, user) {
131
+ try {
132
+ await api.delete(`${v1(user)}/products/${productId}`);
133
+ return true;
134
+ } catch (e) {
135
+ if (e.response?.status === 404) return false;
136
+ throw apiError(e);
137
+ }
138
+ }
139
+
140
+ // ─── Low-level: product API resources (profile/api/<name>) ───────────
141
+
142
+ /** Returns the `{ resourceName: definition }` map; `{}` when none. */
143
+ export async function getProductApi(productId, user) {
144
+ try {
145
+ const res = await api.get(`${v1(user)}/products/${productId}/profile/api`);
146
+ return res.data || {};
147
+ } catch (e) {
148
+ if (e.response?.status === 404) return {};
149
+ throw apiError(e);
150
+ }
151
+ }
152
+
153
+ export async function setProductApiResource(productId, resourceName, user, payload) {
154
+ try {
155
+ await api.put(`${v1(user)}/products/${productId}/profile/api/${resourceName}`, payload);
156
+ } catch (e) {
157
+ throw apiError(e);
158
+ }
159
+ }
160
+
161
+ export async function deleteProductApiResource(productId, resourceName, user) {
162
+ try {
163
+ await api.delete(`${v1(user)}/products/${productId}/profile/api/${resourceName}`);
164
+ return true;
165
+ } catch (e) {
166
+ if (e.response?.status === 404) return false;
167
+ throw apiError(e);
168
+ }
169
+ }
170
+
171
+ // ─── Low-level: product code link (profile/code) ─────────────────────
172
+
173
+ export async function getProductCode(productId, user) {
174
+ try {
175
+ const res = await api.get(`${v1(user)}/products/${productId}/profile/code`);
176
+ return res.data || {};
177
+ } catch (e) {
178
+ if (e.response?.status === 404) return {};
179
+ throw apiError(e);
180
+ }
181
+ }
182
+
183
+ export async function setProductCode(productId, user, payload) {
184
+ try {
185
+ await api.put(`${v1(user)}/products/${productId}/profile/code`, payload);
186
+ } catch (e) {
187
+ throw apiError(e);
188
+ }
189
+ }
190
+
191
+ // ─── Low-level: device ↔ product binding ─────────────────────────────
192
+
193
+ export async function setDeviceProduct(deviceId, productId, user) {
194
+ try {
195
+ await api.put(`${v1(user)}/devices/${deviceId}`, { product: productId });
196
+ } catch (e) {
197
+ throw apiError(e, { notFound: `Device not found: ${deviceId}` });
198
+ }
199
+ }
200
+
201
+ // ─── Orchestrators ───────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Generic wrapper hosted in the storage's `index.js`. Dispatches the
205
+ * incoming product-API event to the matching script file by name,
206
+ * shelling out via the device's `cmd` resource. Idempotent on
207
+ * subsequent writes — the wrapper only changes if the contract does.
208
+ */
209
+ const PRODUCT_SCRIPT_INDEX_JS = `function script(ev) {
210
+ const fs = require("fs"), path = require("path");
211
+ const dir = path.join(__dirname, "scripts");
212
+ const match = fs.readdirSync(dir).find(f =>
213
+ f === ev.resource || f.replace(/\\.[^.]+$/, "") === ev.resource
214
+ );
215
+ if (!match) throw new Error("script not found: " + ev.resource);
216
+ const body = fs.readFileSync(path.join(dir, match), "utf8");
217
+ return {
218
+ cmd: body, mode: "api", timeout: 30,
219
+ stdin: ev.payload && ev.payload.input !== undefined
220
+ ? JSON.stringify(ev.payload.input) : undefined
221
+ };
222
+ }
223
+ `;
224
+
225
+ /** Drop a product (and, by default, its companion file storage). */
226
+ export async function deleteProductWithStorage(productId, user, { keepStorage = false } = {}) {
227
+ const steps = [];
228
+
229
+ if (await deleteProduct(productId, user)) {
230
+ steps.push(`deleted product "${productId}"`);
231
+ } else {
232
+ steps.push(`product "${productId}" already absent`);
233
+ }
234
+
235
+ if (keepStorage) {
236
+ steps.push(`kept file storage "${productId}" (keepStorage=true)`);
237
+ } else if (await deleteStorage(productId, user)) {
238
+ steps.push(`deleted file storage "${productId}"`);
239
+ } else {
240
+ steps.push(`file storage "${productId}" already absent`);
241
+ }
242
+
243
+ return { steps };
244
+ }
245
+
246
+ /**
247
+ * Install (create or update) a product script. Ensures the product
248
+ * exists, the storage exists, the index.js wrapper is present, the
249
+ * script body is uploaded, the product is enabled and linked to the
250
+ * storage, and the API resource named after the script's stem is
251
+ * registered. Idempotent.
252
+ */
253
+ export async function installProductScript({ product, name, content, user, icon } = {}) {
254
+ if (!product) throw inputError('product is required');
255
+ if (!name) throw inputError('name is required');
256
+ if (typeof content !== 'string') throw inputError('content is required (string)');
257
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
258
+ throw inputError(
259
+ `Invalid script name "${name}" (allowed: letters, digits, dot, dash, underscore)`,
260
+ );
261
+ }
262
+
263
+ const storage = product;
264
+ const stem = name.replace(/\.[^.]+$/, '');
265
+ const steps = [];
266
+
267
+ // 1. Ensure product exists
268
+ if (!(await productExists(product, user))) {
269
+ const usedIcon = icon || 'fab fa-linux';
270
+ await createProduct(product, user, { icon: usedIcon });
271
+ steps.push(`created product "${product}" (icon: ${usedIcon})`);
272
+ }
273
+
274
+ // 2. Ensure storage exists
275
+ if (!(await storageExists(storage, user))) {
276
+ await createStorage(storage, user, { description: `Product scripts for ${product}` });
277
+ steps.push(`created storage "${storage}"`);
278
+ }
279
+
280
+ // 3. Ensure the generic index.js wrapper is in place
281
+ if (!(await storageFileExists(storage, 'index.js', user))) {
282
+ await writeStorageFile(storage, 'index.js', PRODUCT_SCRIPT_INDEX_JS, user);
283
+ steps.push('wrote index.js wrapper');
284
+ }
285
+
286
+ // 4. Upload the script body
287
+ await writeStorageFile(storage, `scripts/${name}`, content, user);
288
+ steps.push(`wrote scripts/${name} (${content.length} bytes)`);
289
+
290
+ // 5. Ensure profile.code.storage points at this storage
291
+ const code = await getProductCode(product, user);
292
+ if (code.storage !== storage) {
293
+ await setProductCode(product, user, { storage, code: '', version: '1.0' });
294
+ steps.push(`linked profile.code.storage → ${storage}`);
295
+ }
296
+
297
+ // 6. Ensure product is enabled
298
+ const prod = await getProduct(product, user);
299
+ if (!prod.enabled) {
300
+ await updateProduct(product, user, { enabled: true });
301
+ steps.push('enabled product');
302
+ }
303
+
304
+ // 7. Create/update the API resource bound to the script stem
305
+ await setProductApiResource(product, stem, user, {
306
+ enabled: true,
307
+ request: {
308
+ data: {
309
+ target: 'resource',
310
+ resource: 'cmd',
311
+ payload_type: 'source_event',
312
+ payload_function: 'script',
313
+ },
314
+ },
315
+ response: { data: { source: 'request_response', payload_type: 'source_payload' } },
316
+ });
317
+ steps.push(`upserted API resource "${stem}"`);
318
+
319
+ return { steps, stem };
320
+ }
321
+
322
+ /** Reverse of installProductScript: drops both the file and the API resource. */
323
+ export async function removeProductScript({ product, name, user } = {}) {
324
+ if (!product) throw inputError('product is required');
325
+ if (!name) throw inputError('name is required');
326
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
327
+ throw inputError(`Invalid script name "${name}"`);
328
+ }
329
+ const stem = name.replace(/\.[^.]+$/, '');
330
+ const steps = [];
331
+
332
+ if (await deleteStorageFile(product, `scripts/${name}`, user)) {
333
+ steps.push(`deleted scripts/${name}`);
334
+ } else {
335
+ steps.push(`scripts/${name} already absent`);
336
+ }
337
+
338
+ if (await deleteProductApiResource(product, stem, user)) {
339
+ steps.push(`deleted API resource "${stem}"`);
340
+ } else {
341
+ steps.push(`API resource "${stem}" already absent`);
342
+ }
343
+
344
+ return { steps, stem };
345
+ }
346
+
347
+ /**
348
+ * List script files in a product's storage and pair them with the
349
+ * matching API resource (or flag as `[orphan]` when the file exists
350
+ * but no API resource references it).
351
+ */
352
+ export async function listProductScripts(productId, user) {
353
+ const allFiles = await listStorageFiles(productId, user);
354
+ const files = allFiles.filter((f) => f.type === 'file' && f.path.startsWith('scripts/'));
355
+ const apis = await getProductApi(productId, user);
356
+ return files.map((f) => {
357
+ const stem = f.name.replace(/\.[^.]+$/, '');
358
+ return {
359
+ name: f.name,
360
+ path: f.path,
361
+ size: f.size,
362
+ stem,
363
+ registered: Boolean(apis[stem]),
364
+ };
365
+ });
366
+ }
367
+
368
+ export async function readProductScript(productId, name, user) {
369
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
370
+ throw inputError(`Invalid script name "${name}"`);
371
+ }
372
+ return readStorageFile(productId, `scripts/${name}`, user);
373
+ }
package/lib/property.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { readConfig } from './config.js';
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 properties
@@ -8,37 +9,15 @@ import api from './api.js';
8
9
  * @returns {Promise<Object>} Device properties
9
10
  */
10
11
  export async function getDeviceProperties(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
- }
12
+ const config = requireConfig();
17
13
 
18
14
  try {
19
- // Fetch device properties
20
15
  const response = await api.get(
21
16
  `/v3/users/${config.username}/devices/${deviceId}/properties`,
22
17
  );
23
-
24
18
  return response.data;
25
19
  } catch (error) {
26
- if (error.response) {
27
- // The request was made and the server responded with a status code
28
- if (error.response.status === 404) {
29
- throw new Error(`Device not found: ${deviceId}`);
30
- } else if (error.response.status === 401) {
31
- throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
32
- } else {
33
- throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
34
- }
35
- } else if (error.request) {
36
- // The request was made but no response was received
37
- throw new Error('No response from server. Please check your connection.');
38
- } else {
39
- // Something happened in setting up the request that triggered an Error
40
- throw new Error(`Error: ${error.message}`);
41
- }
20
+ throw apiError(error, { notFound: `Device not found: ${deviceId}` });
42
21
  }
43
22
  }
44
23
 
@@ -49,38 +28,41 @@ export async function getDeviceProperties(deviceId) {
49
28
  * @returns {Promise<Object>} Property value
50
29
  */
51
30
  export async function getDeviceProperty(deviceId, propertyId) {
52
- // Get configuration
53
- const config = readConfig();
54
-
55
- if (!config.token || !config.server || !config.username) {
56
- throw new Error('Not configured. Run thinr without parameters to set up.');
57
- }
31
+ const config = requireConfig();
58
32
 
59
33
  try {
60
- // Fetch property value
61
34
  const response = await api.get(
62
- `/v3/users/${config.username}/devices/${deviceId}/properties/${propertyId}`
35
+ `/v3/users/${config.username}/devices/${deviceId}/properties/${propertyId}`,
63
36
  );
64
-
65
37
  return response.data.value;
66
38
  } catch (error) {
67
- console.log(error);
68
- if (error.response) {
69
- // The request was made and the server responded with a status code
70
- if (error.response.status === 404) {
71
- throw new Error(`Property not found: ${propertyId} on device ${deviceId}`);
72
- } else if (error.response.status === 401) {
73
- throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
74
- } else {
75
- throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
76
- }
77
- } else if (error.request) {
78
- // The request was made but no response was received
79
- throw new Error('No response from server. Please check your connection.');
80
- } else {
81
- // Something happened in setting up the request that triggered an Error
82
- throw new Error(`Error: ${error.message}`);
83
- }
39
+ throw apiError(error, {
40
+ notFound: `Property not found: ${propertyId} on device ${deviceId}`,
41
+ });
42
+ }
43
+ }
44
+
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
+ */
55
+ export async function setDeviceProperty(deviceId, propertyId, value) {
56
+ const config = requireConfig();
57
+
58
+ try {
59
+ const response = await api.put(
60
+ `/v3/users/${config.username}/devices/${deviceId}/properties/${propertyId}`,
61
+ { value },
62
+ );
63
+ return response.data;
64
+ } catch (error) {
65
+ throw apiError(error, { notFound: `Device not found: ${deviceId}` });
84
66
  }
85
67
  }
86
68
 
@@ -91,12 +73,11 @@ export async function getDeviceProperty(deviceId, propertyId) {
91
73
  * @returns {string} Formatted properties
92
74
  */
93
75
  export function formatDeviceProperties(deviceId, properties) {
94
-
95
76
  let formattedProperties = '\nProperties of device ' + chalk.cyan(deviceId) + ':\n';
96
77
 
97
- properties.forEach(property => {
78
+ properties.forEach((property) => {
98
79
  formattedProperties += `- ${property.property}\n`;
99
- })
80
+ });
100
81
 
101
82
  return formattedProperties;
102
83
  }
@@ -110,7 +91,12 @@ export function formatDeviceProperties(deviceId, properties) {
110
91
  */
111
92
  export function formatDeviceProperty(deviceId, propertyId, propertyValue) {
112
93
  // show json with flattened structure
113
- let formattedValue = '\nValue of property ' + chalk.cyan(propertyId) + ' for device ' + chalk.cyan(deviceId) + ':\n';
94
+ let formattedValue =
95
+ '\nValue of property ' +
96
+ chalk.cyan(propertyId) +
97
+ ' for device ' +
98
+ chalk.cyan(deviceId) +
99
+ ':\n';
114
100
  const flattenedValue = flatten(propertyValue);
115
101
  for (const key in flattenedValue) {
116
102
  formattedValue += `${key}: ${chalk.green(flattenedValue[key])}\n`;
@@ -130,5 +116,3 @@ function flatten(obj, prefix = '', res = {}) {
130
116
  }
131
117
  return res;
132
118
  }
133
-
134
-