@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.
- package/CHANGELOG.md +86 -0
- package/README.md +117 -123
- package/bin/thinr.js +43 -105
- package/commands/_shared.js +10 -90
- package/commands/device/_shared.js +2 -2
- package/commands/device/exec.js +2 -2
- package/commands/device/fs.js +2 -2
- package/commands/device/index.js +5 -19
- package/commands/device/logs.js +4 -4
- package/commands/device/status.js +1 -1
- package/commands/device/transfer.js +2 -2
- package/commands/device/update.js +8 -2
- package/commands/fleet/upgrade.js +3 -3
- package/commands/playbook/run.js +2 -2
- package/commands/playbook/validate.js +2 -2
- package/commands/product/_shared.js +1 -1
- package/commands/product/exec.js +2 -2
- package/commands/product/fs.js +2 -2
- package/commands/product/index.js +4 -11
- package/commands/product/logs.js +2 -2
- package/commands/product/metric.js +2 -2
- package/commands/product/playbook.js +5 -5
- package/commands/product/property.js +4 -4
- package/commands/product/push.js +2 -2
- package/commands/product/resource.js +4 -4
- package/commands/product/status.js +3 -3
- package/dist/dashboard.js +12 -12
- package/docs/guides/thinremote-metrics-logs.md +59 -0
- package/docs/guides/thinremote-playbooks.md +133 -0
- package/docs/guides/thinremote-scripts.md +115 -0
- package/docs/guides/thinremote.md +116 -0
- package/lib/command-stream.js +3 -2
- package/lib/console.js +6 -6
- package/lib/constants.js +1 -19
- package/lib/dashboard/event-stream.js +5 -5
- package/lib/device-api.js +4 -4
- package/lib/guides.js +41 -0
- package/lib/identity.js +75 -0
- package/lib/local-files.js +54 -0
- package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
- package/lib/mcp/helpers.js +5 -25
- package/lib/mcp/playbook-ops.js +393 -0
- package/lib/mcp/registry.js +9 -43
- package/lib/mcp/resource-hooks.js +53 -0
- package/lib/mcp/resource-types.js +857 -0
- package/lib/mcp/tools-exec.js +217 -177
- package/lib/monitoring.js +3 -3
- package/lib/playbook/actions.js +30 -18
- package/lib/playbook/expression.js +1 -4
- package/lib/playbook/fleet.js +4 -1
- package/lib/playbook/loader.js +16 -8
- package/lib/playbook/runner.js +40 -8
- package/lib/playbook/schema.js +108 -34
- package/lib/product/log-presets.js +1 -1
- package/lib/product/logs.js +2 -2
- package/lib/product-orchestrator.js +14 -9
- package/lib/product.js +29 -227
- package/lib/proxy.js +4 -4
- package/lib/status.js +13 -5
- package/package.json +4 -2
- package/commands/device/delete.js +0 -53
- package/commands/device/list.js +0 -55
- package/commands/device/property.js +0 -52
- package/commands/device/resource.js +0 -52
- package/commands/logout.js +0 -47
- package/commands/product/list.js +0 -86
- package/commands/product/props.js +0 -125
- package/commands/profile.js +0 -173
- package/lib/alarms.js +0 -223
- package/lib/api.js +0 -89
- package/lib/auth.js +0 -428
- package/lib/bucket.js +0 -82
- package/lib/concurrency.js +0 -52
- package/lib/config.js +0 -230
- package/lib/devices.js +0 -60
- package/lib/errors.js +0 -65
- package/lib/format.js +0 -114
- package/lib/mcp/server.js +0 -67
- package/lib/mcp/tools-alarms.js +0 -507
- package/lib/mcp/tools-buckets.js +0 -99
- package/lib/mcp/tools-devices.js +0 -235
- package/lib/mcp/tools-filesystem.js +0 -281
- package/lib/mcp/tools-logs.js +0 -225
- package/lib/mcp/tools-playbook.js +0 -210
- package/lib/mcp/tools-products.js +0 -1557
- package/lib/mcp/tools-profile.js +0 -635
- package/lib/mcp/tools-profiles.js +0 -30
- package/lib/mcp/tools-properties.js +0 -79
- package/lib/mcp/tools-resources.js +0 -78
- package/lib/mcp/tools-scripts.js +0 -212
- package/lib/mcp/tools-tokens.js +0 -369
- package/lib/output.js +0 -114
- package/lib/paths.js +0 -32
- package/lib/profile.js +0 -657
- package/lib/property.js +0 -75
- package/lib/resource.js +0 -151
- package/lib/storage.js +0 -117
- package/lib/tokens.js +0 -366
package/lib/product.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import api from '
|
|
2
|
-
import { apiError, inputError } from '
|
|
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 '
|
|
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 '
|
|
25
|
+
import { v1 } from '@thinger-io/cli-core/paths';
|
|
15
26
|
|
|
16
27
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
822
|
-
? report.name
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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 '
|
|
4
|
-
import api from '
|
|
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 '
|
|
7
|
-
import { info, accent, hint, warning, error as errorStyle } from '
|
|
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 '
|
|
2
|
-
import { v1 } from '
|
|
3
|
-
import api from '
|
|
4
|
-
import { apiError } from '
|
|
5
|
-
import {
|
|
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
|
+
"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": ">=
|
|
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
|
-
}
|
package/commands/device/list.js
DELETED
|
@@ -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
|
-
}
|
package/commands/logout.js
DELETED
|
@@ -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
|
-
}
|