@thinremote/thinr-cli 1.2.0-alpha.1 → 1.2.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 +45 -1
- package/README.md +192 -16
- package/bin/thinr.js +23 -24
- package/commands/_shared.js +154 -0
- package/commands/dashboard.js +17 -0
- package/commands/device/_shared.js +17 -34
- package/commands/device/console.js +1 -0
- package/commands/device/delete.js +53 -0
- package/commands/device/exec.js +7 -6
- package/commands/device/fs.js +193 -0
- package/commands/device/index.js +9 -0
- package/commands/device/list.js +35 -23
- package/commands/device/logs.js +178 -0
- package/commands/device/property.js +30 -28
- package/commands/device/proxy.js +3 -0
- package/commands/device/resource.js +30 -29
- package/commands/device/status.js +35 -14
- package/commands/device/transfer.js +236 -0
- package/commands/device/update.js +15 -10
- package/commands/fleet/index.js +17 -0
- package/commands/fleet/upgrade.js +275 -0
- package/commands/logout.js +10 -11
- package/commands/playbook/_shared.js +12 -0
- package/commands/playbook/index.js +18 -0
- package/commands/playbook/run.js +302 -0
- package/commands/playbook/validate.js +38 -0
- package/commands/product/_shared.js +8 -0
- package/commands/product/exec.js +243 -0
- package/commands/product/fs.js +218 -0
- package/commands/product/index.js +35 -0
- package/commands/product/list.js +86 -0
- package/commands/product/logs.js +289 -0
- package/commands/product/metric.js +124 -0
- package/commands/product/playbook.js +911 -0
- package/commands/product/property.js +76 -0
- package/commands/product/props.js +125 -0
- package/commands/product/push.js +184 -0
- package/commands/product/resource.js +76 -0
- package/commands/product/status.js +213 -0
- package/commands/profile.js +76 -8
- package/dist/dashboard.js +5644 -0
- package/lib/agent-releases.js +59 -0
- package/lib/agent-versions.js +73 -0
- package/lib/alarms.js +252 -0
- package/lib/api.js +8 -2
- package/lib/auth.js +30 -17
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +199 -0
- package/lib/config.js +5 -3
- package/lib/console.js +12 -4
- package/lib/constants.js +76 -0
- package/lib/dashboard/event-stream.js +239 -0
- package/lib/dashboard/metric-aggregate.js +64 -0
- package/lib/dashboard/metric-filter.js +39 -0
- package/lib/debug-log.js +93 -0
- package/lib/device-api.js +109 -147
- package/lib/devices.js +18 -21
- package/lib/fleet-upgrade.js +106 -0
- package/lib/format.js +114 -0
- package/lib/mcp/registry.js +14 -0
- package/lib/mcp/tools-alarms.js +510 -0
- package/lib/mcp/tools-buckets.js +103 -0
- package/lib/mcp/tools-devices.js +33 -3
- package/lib/mcp/tools-exec.js +30 -17
- package/lib/mcp/tools-filesystem.js +102 -6
- package/lib/mcp/tools-install.js +153 -0
- package/lib/mcp/tools-logs.js +225 -0
- package/lib/mcp/tools-playbook.js +211 -0
- package/lib/mcp/tools-products.js +1226 -105
- package/lib/mcp/tools-profile.js +634 -0
- package/lib/mcp/tools-tokens.js +371 -0
- package/lib/monitoring.js +26 -1
- package/lib/playbook/actions.js +447 -0
- package/lib/playbook/expression.js +302 -0
- package/lib/playbook/fleet.js +175 -0
- package/lib/playbook/loader.js +247 -0
- package/lib/playbook/runner.js +300 -0
- package/lib/playbook/schema.js +209 -0
- package/lib/playbook/vars.js +209 -0
- package/lib/product/log-presets.js +196 -0
- package/lib/product/logs.js +485 -0
- package/lib/product-orchestrator.js +124 -0
- package/lib/product.js +523 -20
- package/lib/profile.js +665 -0
- package/lib/property.js +7 -40
- package/lib/proxy.js +9 -8
- package/lib/resource.js +10 -14
- package/lib/status.js +78 -37
- package/lib/tokens.js +374 -0
- package/package.json +14 -3
- package/commands/product.js +0 -407
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import api from './api.js';
|
|
3
|
+
import { apiError, inputError } from './errors.js';
|
|
4
|
+
import { requireConfig } from './config.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helpers for ThinRemote access tokens — both user-level (broad scope,
|
|
8
|
+
* `allow`/`deny` resource trees) and device-level (narrow, scoped to
|
|
9
|
+
* one device's resources).
|
|
10
|
+
*
|
|
11
|
+
* REST endpoints wrapped here:
|
|
12
|
+
*
|
|
13
|
+
* User-level
|
|
14
|
+
* ──────────
|
|
15
|
+
* GET /v1/users/{user}/tokens → list (no JWT)
|
|
16
|
+
* POST /v1/users/{user}/tokens → create (returns full doc + JWT)
|
|
17
|
+
* GET /v1/users/{user}/tokens/{tokenId} → read full doc + JWT
|
|
18
|
+
* PUT /v1/users/{user}/tokens/{tokenId} → patch
|
|
19
|
+
* DELETE /v1/users/{user}/tokens/{tokenId} → delete
|
|
20
|
+
*
|
|
21
|
+
* Device-level
|
|
22
|
+
* ────────────
|
|
23
|
+
* GET /v1/users/{user}/devices/{d}/tokens → list
|
|
24
|
+
* POST /v1/users/{user}/devices/{d}/tokens → create
|
|
25
|
+
* DELETE /v1/users/{user}/devices/{d}/tokens/{tokenId} → delete
|
|
26
|
+
*
|
|
27
|
+
* Naming
|
|
28
|
+
* ──────
|
|
29
|
+
* The user-level CREATE_SCHEMA accepts `token` (the id), `name`, `enabled`,
|
|
30
|
+
* `allow`, `deny`, `expire`, `description`. The device-level POST accepts
|
|
31
|
+
* a different field set: `token_name`, `token_resources`, `token_expiration`.
|
|
32
|
+
* See `tokens.cpp` and `devices.cpp` in the backend for the canonical
|
|
33
|
+
* shape. Helpers here keep the two distinct so callers don't get them
|
|
34
|
+
* confused.
|
|
35
|
+
*
|
|
36
|
+
* Permission grammar (user-level only)
|
|
37
|
+
* ────────────────────────────────────
|
|
38
|
+
* allow / deny: {
|
|
39
|
+
* "<ResourceType>": { "<id|*>": ["<Action>" | "*"] | "*" }
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* Resource types are the platform's resource categories (`Device`,
|
|
43
|
+
* `Bucket`, `Product`, `Endpoint`, `Property`, `Token`, `User`, …) plus
|
|
44
|
+
* `*` (any). The id level can be a literal id or `*` for any. The leaf is
|
|
45
|
+
* either an array of action strings (e.g. `["AccessDeviceResources"]`) or
|
|
46
|
+
* the wildcard string `"*"`.
|
|
47
|
+
*
|
|
48
|
+
* The grammar does NOT scope by *product*; only by resource type + id.
|
|
49
|
+
* To restrict a token to a single product's devices you would have to
|
|
50
|
+
* enumerate the device ids explicitly, or rely on a product-shaped naming
|
|
51
|
+
* convention — see `tokens.cpp:validate_token_permissions` and
|
|
52
|
+
* `tokens.hpp:PERMISSION_DEFINITION`. The MCP descriptions explicitly
|
|
53
|
+
* call this out so callers don't bake in the wrong assumption.
|
|
54
|
+
*
|
|
55
|
+
* JWT visibility
|
|
56
|
+
* ──────────────
|
|
57
|
+
* The list projection on the server omits `access_token`. The full JWT is
|
|
58
|
+
* returned only by the per-token GET (and once on creation). Treat the
|
|
59
|
+
* value as one-shot-readable: persist or hand it off as soon as you fetch
|
|
60
|
+
* it.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
function resolveUser(user) {
|
|
64
|
+
return user || requireConfig().username;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function v1(user) {
|
|
68
|
+
return `/v1/users/${resolveUser(user)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Duration helpers ───────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const DURATION_RE = /^(\d+)([smhdwy])$/;
|
|
74
|
+
const DURATION_MULT = {
|
|
75
|
+
s: 1,
|
|
76
|
+
m: 60,
|
|
77
|
+
h: 60 * 60,
|
|
78
|
+
d: 24 * 60 * 60,
|
|
79
|
+
w: 7 * 24 * 60 * 60,
|
|
80
|
+
y: 365 * 24 * 60 * 60,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert a relative duration string (e.g. "30d", "12h", "365d") or an
|
|
85
|
+
* absolute unix-seconds timestamp into a unix-seconds expiry. Returns
|
|
86
|
+
* `null` for null/undefined input so callers can pass through "no
|
|
87
|
+
* expiry" without branching.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveExpiry(value) {
|
|
90
|
+
if (value === undefined || value === null || value === '') return null;
|
|
91
|
+
if (typeof value === 'number') {
|
|
92
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
93
|
+
throw inputError('expiry must be a non-negative number of seconds');
|
|
94
|
+
}
|
|
95
|
+
return Math.floor(value);
|
|
96
|
+
}
|
|
97
|
+
if (typeof value !== 'string') {
|
|
98
|
+
throw inputError('expiry must be a number, a duration string ("30d") or null');
|
|
99
|
+
}
|
|
100
|
+
const trimmed = value.trim();
|
|
101
|
+
if (/^\d+$/.test(trimmed)) {
|
|
102
|
+
return Number.parseInt(trimmed, 10);
|
|
103
|
+
}
|
|
104
|
+
const m = DURATION_RE.exec(trimmed);
|
|
105
|
+
if (!m) {
|
|
106
|
+
throw inputError(
|
|
107
|
+
`expiry "${value}" not understood. Use a unix-seconds number or a relative duration like "30d", "12h", "1y".`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const n = Number.parseInt(m[1], 10);
|
|
111
|
+
const unit = m[2];
|
|
112
|
+
if (n <= 0) throw inputError('expiry duration must be positive');
|
|
113
|
+
return Math.floor(Date.now() / 1000) + n * DURATION_MULT[unit];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── User-level token validators ────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export const TOKEN_ID_RE = /^[a-zA-Z0-9_]{1,50}$/;
|
|
119
|
+
|
|
120
|
+
function assertValidTokenId(id) {
|
|
121
|
+
if (typeof id !== 'string' || !TOKEN_ID_RE.test(id)) {
|
|
122
|
+
throw inputError(
|
|
123
|
+
'token_id must match /^[a-zA-Z0-9_]{1,50}$/ (letters, digits, underscore, ≤ 50 chars)',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Client-side mirror of the server-side `validate_token_permissions`
|
|
130
|
+
* check. Catches grammar mistakes before the request leaves the box so
|
|
131
|
+
* callers get a precise error pointing at the offending node.
|
|
132
|
+
*/
|
|
133
|
+
export function validatePermissionTree(node, label = 'allow') {
|
|
134
|
+
if (node === undefined || node === null) return;
|
|
135
|
+
if (typeof node !== 'object' || Array.isArray(node)) {
|
|
136
|
+
throw inputError(`${label} must be an object keyed by ResourceType`);
|
|
137
|
+
}
|
|
138
|
+
for (const [type, byId] of Object.entries(node)) {
|
|
139
|
+
if (!/^[a-zA-Z0-9_-]{1,32}$|^\*$/.test(type)) {
|
|
140
|
+
throw inputError(
|
|
141
|
+
`${label} contains invalid resource type "${type}" (allowed: A-Z, a-z, 0-9, underscore, dash; up to 32 chars; or "*")`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (typeof byId !== 'object' || Array.isArray(byId) || byId === null) {
|
|
145
|
+
throw inputError(
|
|
146
|
+
`${label}.${type} must be an object keyed by resource id (or "*")`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
for (const [id, actions] of Object.entries(byId)) {
|
|
150
|
+
if (!/^[a-zA-Z0-9_]{1,32}(?:@[a-zA-Z0-9_]{1,32})?$|^\*$/.test(id)) {
|
|
151
|
+
throw inputError(
|
|
152
|
+
`${label}.${type}.${id} is not a valid resource id (letters, digits, underscore, optional @suffix, ≤ 32 chars; or "*")`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (typeof actions === 'string') {
|
|
156
|
+
if (!/^[a-zA-Z0-9_*]{1,64}$/.test(actions) && actions !== '*') {
|
|
157
|
+
throw inputError(
|
|
158
|
+
`${label}.${type}.${id} action "${actions}" is not a valid action name`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
} else if (Array.isArray(actions)) {
|
|
162
|
+
for (const action of actions) {
|
|
163
|
+
if (typeof action === 'string') {
|
|
164
|
+
if (!/^[a-zA-Z0-9_*]{1,64}$/.test(action) && action !== '*') {
|
|
165
|
+
throw inputError(
|
|
166
|
+
`${label}.${type}.${id}: action "${action}" is not a valid action name`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
} else if (typeof action === 'object' && action !== null) {
|
|
170
|
+
// nested object form — minimal shape check.
|
|
171
|
+
for (const k of Object.keys(action)) {
|
|
172
|
+
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(k)) {
|
|
173
|
+
throw inputError(
|
|
174
|
+
`${label}.${type}.${id}: nested action key "${k}" is invalid`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
throw inputError(
|
|
180
|
+
`${label}.${type}.${id}: actions must be strings or nested objects`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
throw inputError(
|
|
186
|
+
`${label}.${type}.${id} must be an array of action strings or the wildcard "*"`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── User-level tokens ──────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
export async function listTokens(user) {
|
|
196
|
+
try {
|
|
197
|
+
const res = await api.get(`${v1(user)}/tokens`);
|
|
198
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
199
|
+
} catch (e) {
|
|
200
|
+
throw apiError(e);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function getToken(tokenId, user) {
|
|
205
|
+
assertValidTokenId(tokenId);
|
|
206
|
+
try {
|
|
207
|
+
const res = await api.get(`${v1(user)}/tokens/${tokenId}`);
|
|
208
|
+
return res.data || {};
|
|
209
|
+
} catch (e) {
|
|
210
|
+
throw apiError(e, { notFound: `Token not found: ${tokenId}` });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a user-level token. Returns the persisted document, including
|
|
216
|
+
* the freshly-issued JWT under `access_token` (only ever returned on
|
|
217
|
+
* create and on per-token GET — list responses strip it).
|
|
218
|
+
*
|
|
219
|
+
* @param {{
|
|
220
|
+
* token: string,
|
|
221
|
+
* name: string,
|
|
222
|
+
* allow: Record<string, Record<string, string[] | string>>,
|
|
223
|
+
* deny?: Record<string, Record<string, string[] | string>>,
|
|
224
|
+
* description?: string,
|
|
225
|
+
* expire?: number | string | null,
|
|
226
|
+
* enabled?: boolean,
|
|
227
|
+
* }} body
|
|
228
|
+
*/
|
|
229
|
+
export async function createToken(body, user) {
|
|
230
|
+
if (!body || typeof body !== 'object') throw inputError('token body is required');
|
|
231
|
+
assertValidTokenId(body.token);
|
|
232
|
+
if (!body.name || typeof body.name !== 'string') {
|
|
233
|
+
throw inputError('token name is required');
|
|
234
|
+
}
|
|
235
|
+
if (!body.allow || typeof body.allow !== 'object') {
|
|
236
|
+
throw inputError(
|
|
237
|
+
'token allow is required (top-level: ResourceType strings; second-level: ids/"*"; leaves: arrays of action strings or "*")',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
validatePermissionTree(body.allow, 'allow');
|
|
241
|
+
validatePermissionTree(body.deny, 'deny');
|
|
242
|
+
|
|
243
|
+
const payload = {
|
|
244
|
+
token: body.token,
|
|
245
|
+
name: body.name,
|
|
246
|
+
enabled: body.enabled === undefined ? true : !!body.enabled,
|
|
247
|
+
allow: body.allow,
|
|
248
|
+
};
|
|
249
|
+
if (body.deny) payload.deny = body.deny;
|
|
250
|
+
if (body.description !== undefined) payload.description = String(body.description);
|
|
251
|
+
const expire = resolveExpiry(body.expire);
|
|
252
|
+
if (expire !== null && expire !== undefined) payload.expire = expire;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const res = await api.post(`${v1(user)}/tokens`, payload);
|
|
256
|
+
return res.data;
|
|
257
|
+
} catch (e) {
|
|
258
|
+
throw apiError(e);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Patch a token. Only the fields accepted by the server's UPDATE_SCHEMA
|
|
264
|
+
* (name, description, expire, enabled, allow, deny) are propagated.
|
|
265
|
+
*/
|
|
266
|
+
export async function updateToken(tokenId, patch, user) {
|
|
267
|
+
assertValidTokenId(tokenId);
|
|
268
|
+
if (!patch || typeof patch !== 'object') throw inputError('patch is required');
|
|
269
|
+
|
|
270
|
+
const body = {};
|
|
271
|
+
if (patch.name !== undefined) body.name = String(patch.name);
|
|
272
|
+
if (patch.description !== undefined) body.description = String(patch.description);
|
|
273
|
+
if (patch.enabled !== undefined) body.enabled = !!patch.enabled;
|
|
274
|
+
if (patch.expire !== undefined) {
|
|
275
|
+
const expire = resolveExpiry(patch.expire);
|
|
276
|
+
if (expire !== null && expire !== undefined) body.expire = expire;
|
|
277
|
+
}
|
|
278
|
+
if (patch.allow !== undefined) {
|
|
279
|
+
validatePermissionTree(patch.allow, 'allow');
|
|
280
|
+
body.allow = patch.allow;
|
|
281
|
+
}
|
|
282
|
+
if (patch.deny !== undefined) {
|
|
283
|
+
validatePermissionTree(patch.deny, 'deny');
|
|
284
|
+
body.deny = patch.deny;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (Object.keys(body).length === 0) {
|
|
288
|
+
throw inputError(
|
|
289
|
+
'patch must set at least one of: name, description, enabled, expire, allow, deny',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const res = await api.put(`${v1(user)}/tokens/${tokenId}`, body);
|
|
295
|
+
return res.data;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
throw apiError(e, { notFound: `Token not found: ${tokenId}` });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function deleteToken(tokenId, user) {
|
|
302
|
+
assertValidTokenId(tokenId);
|
|
303
|
+
try {
|
|
304
|
+
await api.delete(`${v1(user)}/tokens/${tokenId}`);
|
|
305
|
+
return true;
|
|
306
|
+
} catch (e) {
|
|
307
|
+
if (e.response?.status === 404) return false;
|
|
308
|
+
throw apiError(e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── Device-level tokens ────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
export async function listDeviceTokens(deviceId, user) {
|
|
315
|
+
if (!deviceId) throw inputError('device is required');
|
|
316
|
+
try {
|
|
317
|
+
const res = await api.get(`${v1(user)}/devices/${deviceId}/tokens`);
|
|
318
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
319
|
+
} catch (e) {
|
|
320
|
+
throw apiError(e, { notFound: `Device not found: ${deviceId}` });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create a device-scoped token. Server-side payload shape (different from
|
|
326
|
+
* user-level tokens):
|
|
327
|
+
*
|
|
328
|
+
* { token_name, token_resources?, token_expiration? }
|
|
329
|
+
*
|
|
330
|
+
* `token_resources` omitted ⇒ all resources of the device.
|
|
331
|
+
* `token_expiration` omitted ⇒ no expiry.
|
|
332
|
+
*/
|
|
333
|
+
export async function createDeviceToken(deviceId, body, user) {
|
|
334
|
+
if (!deviceId) throw inputError('device is required');
|
|
335
|
+
if (!body || typeof body !== 'object') throw inputError('token body is required');
|
|
336
|
+
if (!body.token_name || typeof body.token_name !== 'string') {
|
|
337
|
+
throw inputError('token_name is required');
|
|
338
|
+
}
|
|
339
|
+
const payload = { token_name: body.token_name };
|
|
340
|
+
if (body.token_resources !== undefined) {
|
|
341
|
+
if (!Array.isArray(body.token_resources)) {
|
|
342
|
+
throw inputError('token_resources must be an array of resource names');
|
|
343
|
+
}
|
|
344
|
+
for (const r of body.token_resources) {
|
|
345
|
+
if (typeof r !== 'string' || !r) {
|
|
346
|
+
throw inputError('token_resources entries must be non-empty strings');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
payload.token_resources = body.token_resources;
|
|
350
|
+
}
|
|
351
|
+
const expiration = resolveExpiry(body.token_expiration);
|
|
352
|
+
if (expiration !== null && expiration !== undefined) {
|
|
353
|
+
payload.token_expiration = expiration;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const res = await api.post(`${v1(user)}/devices/${deviceId}/tokens`, payload);
|
|
358
|
+
return res.data;
|
|
359
|
+
} catch (e) {
|
|
360
|
+
throw apiError(e, { notFound: `Device not found: ${deviceId}` });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function deleteDeviceToken(deviceId, tokenId, user) {
|
|
365
|
+
if (!deviceId) throw inputError('device is required');
|
|
366
|
+
if (!tokenId) throw inputError('token_id is required');
|
|
367
|
+
try {
|
|
368
|
+
await api.delete(`${v1(user)}/devices/${deviceId}/tokens/${tokenId}`);
|
|
369
|
+
return true;
|
|
370
|
+
} catch (e) {
|
|
371
|
+
if (e.response?.status === 404) return false;
|
|
372
|
+
throw apiError(e);
|
|
373
|
+
}
|
|
374
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinremote/thinr-cli",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.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
|
+
"dist",
|
|
13
14
|
"CHANGELOG.md",
|
|
14
15
|
"LICENSE",
|
|
15
16
|
"README.md"
|
|
@@ -27,7 +28,10 @@
|
|
|
27
28
|
"format": "prettier --write .",
|
|
28
29
|
"format:check": "prettier --check .",
|
|
29
30
|
"typecheck": "tsc",
|
|
30
|
-
"test": "node --test 'tests/**/*.test.js'"
|
|
31
|
+
"test": "node --test 'tests/**/*.test.js'",
|
|
32
|
+
"build:dashboard": "node scripts/build-dashboard.mjs",
|
|
33
|
+
"build:dashboard:watch": "node scripts/build-dashboard.mjs --watch",
|
|
34
|
+
"prepare": "node scripts/build-dashboard.mjs"
|
|
31
35
|
},
|
|
32
36
|
"keywords": [
|
|
33
37
|
"cli",
|
|
@@ -42,18 +46,24 @@
|
|
|
42
46
|
"author": "ThinRemote",
|
|
43
47
|
"license": "MIT",
|
|
44
48
|
"dependencies": {
|
|
49
|
+
"@inkjs/ui": "^2.0.0",
|
|
45
50
|
"@inquirer/prompts": "^8.4.1",
|
|
46
51
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
52
|
"axios": "^1.10.0",
|
|
48
53
|
"chalk": "^5.4.1",
|
|
54
|
+
"cli-progress": "^3.12.0",
|
|
49
55
|
"cli-table3": "^0.6.5",
|
|
50
56
|
"commander": "^14.0.0",
|
|
51
57
|
"conf": "^15.1.0",
|
|
52
58
|
"figlet": "^1.8.1",
|
|
59
|
+
"ink": "^7.0.1",
|
|
60
|
+
"ink-gradient": "^4.0.0",
|
|
53
61
|
"jsonwebtoken": "^9.0.2",
|
|
54
62
|
"open": "^11.0.0",
|
|
55
63
|
"ora": "^9.3.0",
|
|
56
|
-
"
|
|
64
|
+
"react": "^19.2.5",
|
|
65
|
+
"ws": "^8.18.2",
|
|
66
|
+
"yaml": "^2.8.3"
|
|
57
67
|
},
|
|
58
68
|
"type": "module",
|
|
59
69
|
"engines": {
|
|
@@ -62,6 +72,7 @@
|
|
|
62
72
|
"devDependencies": {
|
|
63
73
|
"@eslint/js": "^10.0.1",
|
|
64
74
|
"@types/node": "^25.6.0",
|
|
75
|
+
"esbuild": "^0.28.0",
|
|
65
76
|
"eslint": "^10.2.0",
|
|
66
77
|
"eslint-config-prettier": "^10.1.8",
|
|
67
78
|
"globals": "^17.5.0",
|