@thinremote/thinr-cli 1.3.4 → 1.4.0

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 (98) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +117 -123
  3. package/bin/thinr.js +43 -105
  4. package/commands/_shared.js +10 -90
  5. package/commands/device/_shared.js +2 -2
  6. package/commands/device/exec.js +2 -2
  7. package/commands/device/fs.js +2 -2
  8. package/commands/device/index.js +5 -19
  9. package/commands/device/logs.js +4 -4
  10. package/commands/device/status.js +1 -1
  11. package/commands/device/transfer.js +2 -2
  12. package/commands/device/update.js +8 -2
  13. package/commands/fleet/upgrade.js +3 -3
  14. package/commands/playbook/run.js +2 -2
  15. package/commands/playbook/validate.js +2 -2
  16. package/commands/product/_shared.js +1 -1
  17. package/commands/product/exec.js +2 -2
  18. package/commands/product/fs.js +2 -2
  19. package/commands/product/index.js +4 -11
  20. package/commands/product/logs.js +2 -2
  21. package/commands/product/metric.js +2 -2
  22. package/commands/product/playbook.js +5 -5
  23. package/commands/product/property.js +4 -4
  24. package/commands/product/push.js +2 -2
  25. package/commands/product/resource.js +4 -4
  26. package/commands/product/status.js +3 -3
  27. package/dist/dashboard.js +12 -12
  28. package/docs/guides/thinremote-metrics-logs.md +59 -0
  29. package/docs/guides/thinremote-playbooks.md +133 -0
  30. package/docs/guides/thinremote-scripts.md +115 -0
  31. package/docs/guides/thinremote.md +116 -0
  32. package/lib/command-stream.js +3 -2
  33. package/lib/console.js +6 -6
  34. package/lib/constants.js +1 -19
  35. package/lib/dashboard/event-stream.js +5 -5
  36. package/lib/device-api.js +4 -4
  37. package/lib/guides.js +41 -0
  38. package/lib/identity.js +75 -0
  39. package/lib/local-files.js +54 -0
  40. package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
  41. package/lib/mcp/helpers.js +5 -25
  42. package/lib/mcp/playbook-ops.js +393 -0
  43. package/lib/mcp/registry.js +9 -43
  44. package/lib/mcp/resource-hooks.js +53 -0
  45. package/lib/mcp/resource-types.js +857 -0
  46. package/lib/mcp/tools-exec.js +217 -177
  47. package/lib/monitoring.js +3 -3
  48. package/lib/playbook/actions.js +30 -18
  49. package/lib/playbook/expression.js +1 -4
  50. package/lib/playbook/fleet.js +4 -1
  51. package/lib/playbook/loader.js +16 -8
  52. package/lib/playbook/runner.js +40 -8
  53. package/lib/playbook/schema.js +108 -34
  54. package/lib/product/log-presets.js +1 -1
  55. package/lib/product/logs.js +2 -2
  56. package/lib/product-orchestrator.js +14 -9
  57. package/lib/product.js +29 -227
  58. package/lib/proxy.js +4 -4
  59. package/lib/status.js +13 -5
  60. package/package.json +4 -2
  61. package/commands/device/delete.js +0 -53
  62. package/commands/device/list.js +0 -55
  63. package/commands/device/property.js +0 -52
  64. package/commands/device/resource.js +0 -52
  65. package/commands/logout.js +0 -47
  66. package/commands/product/list.js +0 -86
  67. package/commands/product/props.js +0 -125
  68. package/commands/profile.js +0 -173
  69. package/lib/alarms.js +0 -223
  70. package/lib/api.js +0 -89
  71. package/lib/auth.js +0 -428
  72. package/lib/bucket.js +0 -82
  73. package/lib/concurrency.js +0 -52
  74. package/lib/config.js +0 -230
  75. package/lib/devices.js +0 -60
  76. package/lib/errors.js +0 -65
  77. package/lib/format.js +0 -114
  78. package/lib/mcp/server.js +0 -67
  79. package/lib/mcp/tools-alarms.js +0 -507
  80. package/lib/mcp/tools-buckets.js +0 -99
  81. package/lib/mcp/tools-devices.js +0 -235
  82. package/lib/mcp/tools-filesystem.js +0 -281
  83. package/lib/mcp/tools-logs.js +0 -225
  84. package/lib/mcp/tools-playbook.js +0 -210
  85. package/lib/mcp/tools-products.js +0 -1557
  86. package/lib/mcp/tools-profile.js +0 -635
  87. package/lib/mcp/tools-profiles.js +0 -30
  88. package/lib/mcp/tools-properties.js +0 -79
  89. package/lib/mcp/tools-resources.js +0 -78
  90. package/lib/mcp/tools-scripts.js +0 -212
  91. package/lib/mcp/tools-tokens.js +0 -369
  92. package/lib/output.js +0 -114
  93. package/lib/paths.js +0 -32
  94. package/lib/profile.js +0 -657
  95. package/lib/property.js +0 -75
  96. package/lib/resource.js +0 -151
  97. package/lib/storage.js +0 -117
  98. package/lib/tokens.js +0 -366
package/lib/product.js CHANGED
@@ -1,5 +1,5 @@
1
- import api from './api.js';
2
- import { apiError, inputError } from './errors.js';
1
+ import api from '@thinger-io/cli-core/api';
2
+ import { apiError, inputError } from '@thinger-io/cli-core/errors';
3
3
  import {
4
4
  storageExists,
5
5
  createStorage,
@@ -9,17 +9,25 @@ import {
9
9
  storageFileExists,
10
10
  writeStorageFile,
11
11
  deleteStorageFile,
12
- } from './storage.js';
12
+ } from '@thinger-io/cli-core/storage';
13
+ import {
14
+ deleteProduct,
15
+ deleteProductProperty,
16
+ getProduct,
17
+ getProductApi,
18
+ getProductProperty,
19
+ productExists,
20
+ setProductProperty,
21
+ updateProduct,
22
+ } from '@thinger-io/cli-core/product';
23
+ import { deleteProductApiResource, setProductApiResource } from '@thinger-io/cli-core/profile';
13
24
  import { parsePlaybook } from './playbook/loader.js';
14
- import { v1 } from './paths.js';
25
+ import { v1 } from '@thinger-io/cli-core/paths';
15
26
 
16
27
  /**
17
- * Helpers for ThinRemote products and the product-script workflow.
18
- *
19
- * Low-level helpers (`getProducts`, `getProduct`, `createProduct`, …)
20
- * each map to a single REST endpoint and throw `apiError` on failure
21
- * — except `*Exists` (boolean) and `delete*` (true if removed, false
22
- * if the resource was already absent).
28
+ * ThinRemote's product workflows: product scripts, dashboard metrics and
29
+ * playbooks. The generic product helpers (CRUD, properties, API
30
+ * resources) come from @thinger-io/cli-core.
23
31
  *
24
32
  * Higher-level orchestrators (`installProductScript`,
25
33
  * `removeProductScript`, `deleteProductWithStorage`) compose those
@@ -28,151 +36,6 @@ import { v1 } from './paths.js';
28
36
  * sequence.
29
37
  */
30
38
 
31
- // ─── Low-level: products ─────────────────────────────────────────────
32
-
33
- export async function getProducts(user) {
34
- try {
35
- const res = await api.get(`${v1(user)}/products`);
36
- return res.data || [];
37
- } catch (e) {
38
- throw apiError(e);
39
- }
40
- }
41
-
42
- export async function getProduct(productId, user) {
43
- try {
44
- const res = await api.get(`${v1(user)}/products/${productId}`);
45
- return res.data || {};
46
- } catch (e) {
47
- throw apiError(e, { notFound: `Product not found: ${productId}` });
48
- }
49
- }
50
-
51
- export async function productExists(productId, user) {
52
- try {
53
- await api.get(`${v1(user)}/products/${productId}`);
54
- return true;
55
- } catch (e) {
56
- if (e.response?.status === 404) return false;
57
- throw apiError(e);
58
- }
59
- }
60
-
61
- /**
62
- * Create a product with the standard ThinRemote scaffold: `type:
63
- * thinremote`, a Font Awesome icon (defaults to Linux), and a
64
- * `monitoring` bucket fed every 60 s from the device's `monitoring`
65
- * resource. Caller controls only `icon`, `name`, `description`.
66
- */
67
- export async function createProduct(
68
- productId,
69
- user,
70
- { icon = 'fab fa-linux', name, description } = {},
71
- ) {
72
- // Icons accept either a Font Awesome class (string, legacy short form)
73
- // or a structured object { type: 'fa'|'svg', source, color, background }.
74
- // The structured form lets callers ship inline SVG markup directly.
75
- const iconSpec =
76
- typeof icon === 'object' && icon !== null
77
- ? {
78
- type: icon.type === 'svg' ? 'svg' : 'fa',
79
- source: icon.source,
80
- color: icon.color || '#000000',
81
- background: icon.background || '#ffffff',
82
- }
83
- : { type: 'fa', source: icon, color: '#000000', background: '#ffffff' };
84
- try {
85
- await api.post(`${v1(user)}/products`, {
86
- product: productId,
87
- name: name || productId,
88
- description: description || 'ThinRemote product',
89
- enabled: true,
90
- config: {
91
- type: 'thinremote',
92
- icons: [
93
- {
94
- conditions: [],
95
- icon: iconSpec,
96
- },
97
- ],
98
- },
99
- profile: {
100
- buckets: {
101
- monitoring: {
102
- name: 'Monitoring Data',
103
- description: 'ThinRemote device metrics',
104
- enabled: true,
105
- backend: 'mongodb',
106
- data: {
107
- source: 'resource',
108
- resource: 'monitoring',
109
- update: 'interval',
110
- interval: 60,
111
- magnitude: 'second',
112
- payload: '{{payload}}',
113
- payload_function: '',
114
- payload_type: 'source_payload',
115
- },
116
- retention: { period: 3, unit: 'months' },
117
- tags: [],
118
- },
119
- },
120
- },
121
- });
122
- } catch (e) {
123
- throw apiError(e);
124
- }
125
- }
126
-
127
- export async function updateProduct(productId, user, patch) {
128
- try {
129
- await api.put(`${v1(user)}/products/${productId}`, patch);
130
- } catch (e) {
131
- throw apiError(e, { notFound: `Product not found: ${productId}` });
132
- }
133
- }
134
-
135
- export async function deleteProduct(productId, user) {
136
- try {
137
- await api.delete(`${v1(user)}/products/${productId}`);
138
- return true;
139
- } catch (e) {
140
- if (e.response?.status === 404) return false;
141
- throw apiError(e);
142
- }
143
- }
144
-
145
- // ─── Low-level: product API resources (profile/api/<name>) ───────────
146
-
147
- /** Returns the `{ resourceName: definition }` map; `{}` when none. */
148
- export async function getProductApi(productId, user) {
149
- try {
150
- const res = await api.get(`${v1(user)}/products/${productId}/profile/api`);
151
- return res.data || {};
152
- } catch (e) {
153
- if (e.response?.status === 404) return {};
154
- throw apiError(e);
155
- }
156
- }
157
-
158
- export async function setProductApiResource(productId, resourceName, user, payload) {
159
- try {
160
- await api.put(`${v1(user)}/products/${productId}/profile/api/${resourceName}`, payload);
161
- } catch (e) {
162
- throw apiError(e);
163
- }
164
- }
165
-
166
- export async function deleteProductApiResource(productId, resourceName, user) {
167
- try {
168
- await api.delete(`${v1(user)}/products/${productId}/profile/api/${resourceName}`);
169
- return true;
170
- } catch (e) {
171
- if (e.response?.status === 404) return false;
172
- throw apiError(e);
173
- }
174
- }
175
-
176
39
  // ─── Low-level: product code link (profile/code) ─────────────────────
177
40
 
178
41
  export async function getProductCode(productId, user) {
@@ -193,68 +56,6 @@ export async function setProductCode(productId, user, payload) {
193
56
  }
194
57
  }
195
58
 
196
- // ─── Low-level: product properties (/v1/.../products/<id>/properties) ─
197
- //
198
- // Product-level properties are structured JSON values attached to the
199
- // product itself (not to any single device). Typical uses: dashboard
200
- // configuration, shared feature flags, a list of metrics to stream.
201
- // Endpoint mirrors device properties but lives under the v1 product
202
- // namespace on the server.
203
-
204
- export async function getProductProperties(productId, user) {
205
- try {
206
- const res = await api.get(`${v1(user)}/products/${productId}/properties`);
207
- return res.data || [];
208
- } catch (e) {
209
- throw apiError(e, { notFound: `Product not found: ${productId}` });
210
- }
211
- }
212
-
213
- export async function getProductProperty(productId, propertyId, user) {
214
- try {
215
- const res = await api.get(
216
- `${v1(user)}/products/${productId}/properties/${propertyId}`,
217
- );
218
- return res.data?.value;
219
- } catch (e) {
220
- throw apiError(e, {
221
- notFound: `Property not found: ${propertyId} on product ${productId}`,
222
- });
223
- }
224
- }
225
-
226
- export async function setProductProperty(productId, propertyId, value, user) {
227
- try {
228
- const res = await api.put(
229
- `${v1(user)}/products/${productId}/properties/${propertyId}`,
230
- { value },
231
- );
232
- return res.data;
233
- } catch (e) {
234
- throw apiError(e, { notFound: `Product not found: ${productId}` });
235
- }
236
- }
237
-
238
- export async function deleteProductProperty(productId, propertyId, user) {
239
- try {
240
- await api.delete(`${v1(user)}/products/${productId}/properties/${propertyId}`);
241
- return true;
242
- } catch (e) {
243
- if (e.response?.status === 404) return false;
244
- throw apiError(e);
245
- }
246
- }
247
-
248
- // ─── Low-level: device ↔ product binding ─────────────────────────────
249
-
250
- export async function setDeviceProduct(deviceId, productId, user) {
251
- try {
252
- await api.put(`${v1(user)}/devices/${deviceId}`, { product: productId });
253
- } catch (e) {
254
- throw apiError(e, { notFound: `Device not found: ${deviceId}` });
255
- }
256
- }
257
-
258
59
  // ─── Orchestrators ───────────────────────────────────────────────────
259
60
 
260
61
  /**
@@ -380,12 +181,13 @@ export async function installProductScript({ product, name, content, user, input
380
181
 
381
182
  // 1. Refuse to silently create the product. The agent flow is
382
183
  // list → create → write-script: if the product is missing, the
383
- // caller has to create it explicitly with `thinr_product_create`
184
+ // caller has to create it explicitly (`thinr_resource_create` type
185
+ // "product")
384
186
  // so its name, description and icon are set deliberately rather
385
187
  // than picking up defaults from a side effect.
386
188
  if (!(await productExists(product, user))) {
387
189
  const err = new Error(
388
- `Product "${product}" does not exist. Create it first with thinr_product_create.`,
190
+ `Product "${product}" does not exist. Create it first with thinr_resource_create type "product".`,
389
191
  );
390
192
  err.code = 'not_found';
391
193
  throw err;
@@ -745,8 +547,8 @@ export async function uploadProductPlaybook({
745
547
  typeof description === 'string'
746
548
  ? description
747
549
  : typeof parsedDescription === 'string'
748
- ? parsedDescription
749
- : '',
550
+ ? parsedDescription
551
+ : '',
750
552
  path,
751
553
  };
752
554
  const next = [...existing];
@@ -818,12 +620,12 @@ export async function uploadFleetRunReport({ product, report, user, filename } =
818
620
  }
819
621
 
820
622
  function defaultRunReportFilename(report) {
821
- const name = typeof report.name === 'string' && PLAYBOOK_NAME_RE.test(report.name)
822
- ? report.name
823
- : 'run';
824
- const user = typeof report.user === 'string' && /^[A-Za-z0-9_.-]+$/.test(report.user)
825
- ? report.user
826
- : 'unknown';
623
+ const name =
624
+ typeof report.name === 'string' && PLAYBOOK_NAME_RE.test(report.name) ? report.name : 'run';
625
+ const user =
626
+ typeof report.user === 'string' && /^[A-Za-z0-9_.-]+$/.test(report.user)
627
+ ? report.user
628
+ : 'unknown';
827
629
  const ts = typeof report.startedAt === 'string' ? report.startedAt : new Date().toISOString();
828
630
  const stamp = ts.replace(/[:.]/g, '').replace(/[^0-9TZ]/g, '');
829
631
  return `${stamp}-${name}-${user}.json`;
package/lib/proxy.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import open, { apps } from 'open';
2
2
  import ora from 'ora';
3
- import { readConfig, requireConfig } from './config.js';
4
- import api from './api.js';
3
+ import { readConfig, requireConfig } from '@thinger-io/cli-core/config';
4
+ import api from '@thinger-io/cli-core/api';
5
5
  import { PROXY_PORTS } from './constants.js';
6
- import { apiError } from './errors.js';
7
- import { info, accent, hint, warning, error as errorStyle } from './format.js';
6
+ import { apiError } from '@thinger-io/cli-core/errors';
7
+ import { info, accent, hint, warning, error as errorStyle } from '@thinger-io/cli-core/format';
8
8
 
9
9
  /**
10
10
  * Parse target argument into address and port
package/lib/status.js CHANGED
@@ -1,8 +1,16 @@
1
- import { resolveUser } from './config.js';
2
- import { v1 } from './paths.js';
3
- import api from './api.js';
4
- import { apiError } from './errors.js';
5
- import { formatUptime, colorPct, formatBytes, label, muted, success, error } from './format.js';
1
+ import { resolveUser } from '@thinger-io/cli-core/config';
2
+ import { v1 } from '@thinger-io/cli-core/paths';
3
+ import api from '@thinger-io/cli-core/api';
4
+ import { apiError } from '@thinger-io/cli-core/errors';
5
+ import {
6
+ formatUptime,
7
+ colorPct,
8
+ formatBytes,
9
+ label,
10
+ muted,
11
+ success,
12
+ error,
13
+ } from '@thinger-io/cli-core/format';
6
14
 
7
15
  /**
8
16
  * Get device status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinremote/thinr-cli",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for ThinRemote — remote management for IoT devices, with an MCP server for AI clients",
5
5
  "main": "bin/thinr.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin",
11
11
  "commands",
12
12
  "lib",
13
+ "docs/guides",
13
14
  "dist",
14
15
  "CHANGELOG.md",
15
16
  "LICENSE",
@@ -49,6 +50,7 @@
49
50
  "@inkjs/ui": "^2.0.0",
50
51
  "@inquirer/prompts": "^8.4.1",
51
52
  "@modelcontextprotocol/sdk": "^1.29.0",
53
+ "@thinger-io/cli-core": "^0.3.0",
52
54
  "axios": "^1.10.0",
53
55
  "chalk": "^5.4.1",
54
56
  "cli-progress": "^3.12.0",
@@ -67,7 +69,7 @@
67
69
  },
68
70
  "type": "module",
69
71
  "engines": {
70
- "node": ">=18"
72
+ "node": ">=20"
71
73
  },
72
74
  "devDependencies": {
73
75
  "@eslint/js": "^10.0.1",
@@ -1,53 +0,0 @@
1
- // @ts-check
2
- import { confirm } from '@inquirer/prompts';
3
- import { deleteDevice } from '../../lib/devices.js';
4
- import { isJsonMode, printOk, printErr, classifyError } from '../../lib/output.js';
5
- import { hint, info } from '../../lib/format.js';
6
- import { applyJsonFlag, ensureConfigured, getGlobalUser } from './_shared.js';
7
-
8
- export function registerDeleteCommand(device) {
9
- device
10
- .command('delete <deviceId>')
11
- .helpGroup('State & resources:')
12
- .description('Delete a device record from the platform (irreversible)')
13
- .option('-y, --yes', 'Skip the interactive confirmation prompt')
14
- .option('-j, --json', 'Output as JSON')
15
- .action(async (deviceId, opts, cmd) => {
16
- applyJsonFlag(opts);
17
- ensureConfigured();
18
- const user = getGlobalUser(cmd);
19
-
20
- if (!opts.yes && !isJsonMode()) {
21
- if (!process.stdin.isTTY) {
22
- printErr('Refusing to delete without --yes in a non-interactive shell', {
23
- code: 'needs_confirm',
24
- });
25
- return;
26
- }
27
- const ok = await confirm({
28
- message: `Delete device ${deviceId}? This cannot be undone.`,
29
- default: false,
30
- });
31
- if (!ok) {
32
- console.log(hint('Cancelled.'));
33
- return;
34
- }
35
- }
36
-
37
- try {
38
- const removed = await deleteDevice(deviceId, user);
39
- if (isJsonMode()) {
40
- printOk({ device: deviceId, removed });
41
- return;
42
- }
43
- console.log(
44
- removed
45
- ? `Deleted ${info(deviceId)}`
46
- : `Device ${info(deviceId)} did not exist`,
47
- );
48
- } catch (error) {
49
- const { message, code } = classifyError(error);
50
- printErr(message, { code });
51
- }
52
- });
53
- }
@@ -1,55 +0,0 @@
1
- // @ts-check
2
- import { getDevices } from '../../lib/devices.js';
3
- import { isJsonMode, printOk } from '../../lib/output.js';
4
- import { success, hint, label } from '../../lib/format.js';
5
- import {
6
- applyJsonFlag,
7
- ensureConfigured,
8
- getGlobalUser,
9
- runDeviceCommand,
10
- } from './_shared.js';
11
-
12
- export function registerListCommand(device) {
13
- device
14
- .command('list [pattern]')
15
- .helpGroup('Discovery:')
16
- .description(
17
- 'List devices, optionally filtered by name/id pattern (case-insensitive regex)',
18
- )
19
- .option('-j, --json', 'Output as JSON')
20
- .action(async (pattern, opts, cmd) => {
21
- applyJsonFlag(opts);
22
- ensureConfigured();
23
- const user = getGlobalUser(cmd);
24
- const filter = pattern ? { name: pattern } : {};
25
- await runDeviceCommand({
26
- start: pattern
27
- ? `Searching devices matching "${pattern}"...`
28
- : 'Fetching devices...',
29
- fn: () => getDevices(filter, user),
30
- success: (devices) => `Found ${devices.length} device(s)`,
31
- failure: 'Failed to list devices',
32
- onSuccess: (devices) => {
33
- if (isJsonMode()) {
34
- printOk(devices);
35
- return;
36
- }
37
- // Pad the device id column so trailing names line up
38
- // even when ids have very different widths — same
39
- // treatment as thinr product list.
40
- const idWidth = devices.reduce(
41
- (w, d) => Math.max(w, d.device.length),
42
- 0,
43
- );
44
- for (const d of devices) {
45
- const online = d.connection?.active
46
- ? success('online ')
47
- : hint('offline');
48
- const name = d.name ? hint(`(${d.name})`) : '';
49
- const id = label(d.device.padEnd(idWidth));
50
- console.log(` ${online} ${id}${name ? ' ' + name : ''}`);
51
- }
52
- },
53
- });
54
- });
55
- }
@@ -1,52 +0,0 @@
1
- // @ts-check
2
- import {
3
- formatDeviceProperty,
4
- getDeviceProperty,
5
- getDeviceProperties,
6
- formatDeviceProperties,
7
- } from '../../lib/property.js';
8
- import { isJsonMode, printOk } from '../../lib/output.js';
9
- import {
10
- applyJsonFlag,
11
- ensureConfigured,
12
- extractField,
13
- runDeviceCommand,
14
- } from './_shared.js';
15
-
16
- export function registerPropertyCommand(device) {
17
- device
18
- .command('property <deviceId> [propertyId]')
19
- .helpGroup('State & resources:')
20
- .description('List device properties, or read one by ID')
21
- .option('-j, --json', 'Output as JSON')
22
- .option('-f, --field <field>', 'Extract a sub-field (dot path, e.g. data.value)')
23
- .action(async (deviceId, propertyId, opts) => {
24
- applyJsonFlag(opts);
25
- ensureConfigured();
26
- if (!propertyId) {
27
- await runDeviceCommand({
28
- start: `Getting properties for ${deviceId}...`,
29
- fn: () => getDeviceProperties(deviceId),
30
- success: `Properties for ${deviceId} retrieved successfully`,
31
- failure: 'Property retrieval failed',
32
- onSuccess: (result) => {
33
- if (isJsonMode()) printOk(result);
34
- else console.log(formatDeviceProperties(deviceId, result));
35
- },
36
- });
37
- return;
38
- }
39
- await runDeviceCommand({
40
- start: `Retrieving property for ${deviceId}...`,
41
- fn: () => getDeviceProperty(deviceId, propertyId),
42
- success: `Property ${propertyId} for ${deviceId} found`,
43
- failure: 'Property retrieval failed',
44
- onSuccess: (property) => {
45
- const value = extractField(property, opts.field);
46
- if (isJsonMode()) printOk(value);
47
- else if (opts.field) console.log(value);
48
- else console.log(formatDeviceProperty(deviceId, propertyId, property));
49
- },
50
- });
51
- });
52
- }
@@ -1,52 +0,0 @@
1
- // @ts-check
2
- import {
3
- callDeviceResource,
4
- listDeviceResourcesWithSchemas,
5
- formatDeviceResourcesWithSchemas,
6
- } from '../../lib/resource.js';
7
- import { isJsonMode, printOk } from '../../lib/output.js';
8
- import {
9
- applyJsonFlag,
10
- collectInput,
11
- ensureConfigured,
12
- extractField,
13
- runDeviceCommand,
14
- } from './_shared.js';
15
-
16
- export function registerResourceCommand(device) {
17
- device
18
- .command('resource <deviceId> [resource]')
19
- .helpGroup('State & resources:')
20
- .description('List device resources, or call one by name')
21
- .option('-j, --json', 'Output as JSON')
22
- .option('-f, --field <field>', 'Extract a sub-field from the result (dot path)')
23
- .option('-i, --input <key=value>', 'Resource input (repeatable)', collectInput, {})
24
- .action(async (deviceId, resource, opts) => {
25
- applyJsonFlag(opts);
26
- ensureConfigured();
27
- if (!resource) {
28
- await runDeviceCommand({
29
- start: `Getting resources for ${deviceId}...`,
30
- fn: () => listDeviceResourcesWithSchemas(deviceId),
31
- success: `Successfully retrieved resources for ${deviceId}`,
32
- failure: 'Resource retrieval failed',
33
- onSuccess: (entries) => {
34
- if (isJsonMode()) printOk(entries);
35
- else console.log(formatDeviceResourcesWithSchemas(deviceId, entries));
36
- },
37
- });
38
- return;
39
- }
40
- await runDeviceCommand({
41
- start: `Calling resource ${resource} for ${deviceId}...`,
42
- fn: () => callDeviceResource(deviceId, resource, opts.input),
43
- success: `Resource ${resource} returned successfully for ${deviceId}`,
44
- failure: 'Resource call failed',
45
- onSuccess: (result) => {
46
- const value = extractField(result, opts.field);
47
- if (isJsonMode()) printOk(value);
48
- else console.log(value);
49
- },
50
- });
51
- });
52
- }
@@ -1,47 +0,0 @@
1
- import ora from 'ora';
2
- import { deleteConfig, configExists } from '../lib/config.js';
3
- import { warning, error, hint } from '../lib/format.js';
4
-
5
- /**
6
- * Register the logout command
7
- * @param {Command} program - Commander program instance
8
- */
9
- export function logoutCommand(program) {
10
- program
11
- .command('logout')
12
- .helpGroup('Configuration:')
13
- .description('Remove stored credentials and configuration')
14
- .action(async () => {
15
- // Check if configured
16
- if (!configExists()) {
17
- console.log(warning('No configuration found. Already logged out.'));
18
- return;
19
- }
20
-
21
- const spinner = ora('Removing configuration...').start();
22
-
23
- try {
24
- // Delete local configuration
25
- spinner.text = 'Removing local configuration...';
26
- const ok = deleteConfig();
27
-
28
- if (ok) {
29
- spinner.succeed('Logged out successfully. Configuration removed.');
30
- } else {
31
- spinner.fail('Failed to remove configuration.');
32
- console.error(
33
- error(
34
- 'Error: Unable to delete configuration file. You may need to remove it manually.',
35
- ),
36
- );
37
- console.error(
38
- hint('Configuration is stored in ~/.config/thinr-cli/config.json'),
39
- );
40
- }
41
- } catch (err) {
42
- spinner.fail('Failed to remove configuration.');
43
- console.error(error(`Error: ${err.message}`));
44
- console.error(hint('Configuration is stored in ~/.config/thinr-cli/config.json'));
45
- }
46
- });
47
- }