@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/mcp/tools-tokens.js
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import {
|
|
3
|
-
createDeviceToken,
|
|
4
|
-
createToken,
|
|
5
|
-
deleteDeviceToken,
|
|
6
|
-
deleteToken,
|
|
7
|
-
getToken,
|
|
8
|
-
listDeviceTokens,
|
|
9
|
-
listTokens,
|
|
10
|
-
updateToken,
|
|
11
|
-
} from '../tokens.js';
|
|
12
|
-
import { inputError } from '../errors.js';
|
|
13
|
-
|
|
14
|
-
// ─── User-level tokens ──────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
async function toolTokenList(args) {
|
|
17
|
-
const tokens = await listTokens(args.user);
|
|
18
|
-
if (tokens.length === 0) {
|
|
19
|
-
return {
|
|
20
|
-
content: [{ type: 'text', text: 'No access tokens configured.' }],
|
|
21
|
-
isError: false,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
const lines = tokens.map((t) => {
|
|
25
|
-
const enabled = t.enabled === false ? '✗' : '✓';
|
|
26
|
-
const name = t.name || t.token;
|
|
27
|
-
return ` ${enabled} ${t.token} (${name})`;
|
|
28
|
-
});
|
|
29
|
-
return {
|
|
30
|
-
content: [
|
|
31
|
-
{
|
|
32
|
-
type: 'text',
|
|
33
|
-
text: `${tokens.length} token(s):\n${lines.join('\n')}\n\nNote: the access_token JWT is NOT returned by list — use thinr_token_get to read it.`,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
isError: false,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function toolTokenGet(args) {
|
|
41
|
-
if (!args.token_id) throw inputError('token_id is required');
|
|
42
|
-
const doc = await getToken(args.token_id, args.user);
|
|
43
|
-
return {
|
|
44
|
-
content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }],
|
|
45
|
-
isError: false,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function toolTokenCreate(args) {
|
|
50
|
-
if (!args.token_id) throw inputError('token_id is required');
|
|
51
|
-
if (!args.name) throw inputError('name is required');
|
|
52
|
-
if (!args.allow) throw inputError('allow is required (permission tree)');
|
|
53
|
-
|
|
54
|
-
const doc = await createToken(
|
|
55
|
-
{
|
|
56
|
-
token: args.token_id,
|
|
57
|
-
name: args.name,
|
|
58
|
-
allow: args.allow,
|
|
59
|
-
deny: args.deny,
|
|
60
|
-
description: args.description,
|
|
61
|
-
expire: args.expire,
|
|
62
|
-
enabled: args.enabled,
|
|
63
|
-
},
|
|
64
|
-
args.user,
|
|
65
|
-
);
|
|
66
|
-
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text',
|
|
70
|
-
text: `Created token "${args.token_id}". Full document (including access_token JWT):\n${JSON.stringify(doc, null, 2)}\n\nIMPORTANT: persist the JWT now — it will not be visible in thinr_token_list responses; only thinr_token_get returns it.`,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
isError: false,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function toolTokenUpdate(args) {
|
|
78
|
-
if (!args.token_id) throw inputError('token_id is required');
|
|
79
|
-
const patch = {};
|
|
80
|
-
if (args.name !== undefined) patch.name = args.name;
|
|
81
|
-
if (args.description !== undefined) patch.description = args.description;
|
|
82
|
-
if (args.enabled !== undefined) patch.enabled = !!args.enabled;
|
|
83
|
-
if (args.expire !== undefined) patch.expire = args.expire;
|
|
84
|
-
if (args.allow !== undefined) patch.allow = args.allow;
|
|
85
|
-
if (args.deny !== undefined) patch.deny = args.deny;
|
|
86
|
-
|
|
87
|
-
const doc = await updateToken(args.token_id, patch, args.user);
|
|
88
|
-
return {
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: 'text',
|
|
92
|
-
text: `Updated token "${args.token_id}".\n${JSON.stringify(doc, null, 2)}`,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
isError: false,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function toolTokenDelete(args) {
|
|
100
|
-
if (!args.token_id) throw inputError('token_id is required');
|
|
101
|
-
const removed = await deleteToken(args.token_id, args.user);
|
|
102
|
-
return {
|
|
103
|
-
content: [
|
|
104
|
-
{
|
|
105
|
-
type: 'text',
|
|
106
|
-
text: removed
|
|
107
|
-
? `Deleted token "${args.token_id}".`
|
|
108
|
-
: `Token "${args.token_id}" was not configured.`,
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
isError: false,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ─── Device-level tokens ────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
async function toolDeviceTokenList(args) {
|
|
118
|
-
if (!args.device_id) throw inputError('device is required');
|
|
119
|
-
const tokens = await listDeviceTokens(args.device_id, args.user);
|
|
120
|
-
if (tokens.length === 0) {
|
|
121
|
-
return {
|
|
122
|
-
content: [{ type: 'text', text: `No device tokens on device "${args.device_id}".` }],
|
|
123
|
-
isError: false,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
content: [
|
|
128
|
-
{
|
|
129
|
-
type: 'text',
|
|
130
|
-
text: `${tokens.length} device token(s) on "${args.device_id}":\n${JSON.stringify(tokens, null, 2)}`,
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
isError: false,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function toolDeviceTokenCreate(args) {
|
|
138
|
-
if (!args.device_id) throw inputError('device is required');
|
|
139
|
-
if (!args.token_name) throw inputError('token_name is required');
|
|
140
|
-
|
|
141
|
-
const doc = await createDeviceToken(
|
|
142
|
-
args.device_id,
|
|
143
|
-
{
|
|
144
|
-
token_name: args.token_name,
|
|
145
|
-
token_resources: args.token_resources,
|
|
146
|
-
token_expiration: args.token_expiration,
|
|
147
|
-
},
|
|
148
|
-
args.user,
|
|
149
|
-
);
|
|
150
|
-
return {
|
|
151
|
-
content: [
|
|
152
|
-
{
|
|
153
|
-
type: 'text',
|
|
154
|
-
text: `Created device token "${args.token_name}" on "${args.device_id}".\n${JSON.stringify(doc, null, 2)}`,
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
isError: false,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function toolDeviceTokenDelete(args) {
|
|
162
|
-
if (!args.device_id) throw inputError('device is required');
|
|
163
|
-
if (!args.token_id) throw inputError('token_id is required');
|
|
164
|
-
const removed = await deleteDeviceToken(args.device_id, args.token_id, args.user);
|
|
165
|
-
return {
|
|
166
|
-
content: [
|
|
167
|
-
{
|
|
168
|
-
type: 'text',
|
|
169
|
-
text: removed
|
|
170
|
-
? `Deleted device token "${args.token_id}" from "${args.device_id}".`
|
|
171
|
-
: `Device token "${args.token_id}" was not configured on "${args.device_id}".`,
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
isError: false,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ─── Tool registration ───────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
const PERMISSION_GRAMMAR_NOTE = `Permission grammar (allow / deny):
|
|
181
|
-
{ "<ResourceType>": { "<id>"|"*": ["<Action>"|"*"] | "*" } }
|
|
182
|
-
|
|
183
|
-
Resource types are platform categories (Device, Bucket, Product, Endpoint, Property, Token, User, …) plus "*" for any. The id level is a literal id or "*". Leaves are arrays of action strings or the wildcard "*".
|
|
184
|
-
|
|
185
|
-
Limit: the grammar does NOT scope by product. Only by resource type + id. To restrict a token to one product's devices, enumerate device ids explicitly. See \`tokens.cpp:validate_token_permissions\` and \`tokens.hpp:PERMISSION_DEFINITION\` in the backend.
|
|
186
|
-
|
|
187
|
-
Examples (verbatim from the server's CREATE_SCHEMA_EXAMPLE):
|
|
188
|
-
- Admin Access:
|
|
189
|
-
{ token: "admin_access", name: "Admin Access", enabled: true, allow: { "*": { "*": "*" } } }
|
|
190
|
-
Every resource, any name, any action.
|
|
191
|
-
- Shared Access:
|
|
192
|
-
{ token: "shared_access", name: "Shared Access", enabled: true,
|
|
193
|
-
allow: { "Bucket": { "mqtt_bucket": ["ReadBucket"] }, "Device": { "*": ["AccessDeviceResources"] } } }
|
|
194
|
-
Reads from one bucket plus access to any device's resources.`;
|
|
195
|
-
|
|
196
|
-
const EXPIRY_NOTE = `Expiry handling: pass either a unix-seconds number or a relative duration string ("30d", "12h", "1y", "90d"). Strings are converted client-side to absolute unix seconds before the request.`;
|
|
197
|
-
|
|
198
|
-
export const tools = [
|
|
199
|
-
{
|
|
200
|
-
name: 'thinr_token_list',
|
|
201
|
-
description: `List the user-level access tokens configured on the account. The response omits the JWT (\`access_token\` is excluded by the server's LIST_PROJECTION). Use \`thinr_token_get\` to read a specific token's JWT.`,
|
|
202
|
-
inputSchema: {
|
|
203
|
-
type: 'object',
|
|
204
|
-
properties: {
|
|
205
|
-
user: { type: 'string', description: 'API user.' },
|
|
206
|
-
},
|
|
207
|
-
required: [],
|
|
208
|
-
},
|
|
209
|
-
handler: toolTokenList,
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
name: 'thinr_token_get',
|
|
213
|
-
description: `Read a single user-level access token, including the JWT (\`access_token\`). The JWT is shown only by this endpoint and on creation — list responses strip it.`,
|
|
214
|
-
inputSchema: {
|
|
215
|
-
type: 'object',
|
|
216
|
-
properties: {
|
|
217
|
-
token_id: {
|
|
218
|
-
type: 'string',
|
|
219
|
-
description: 'Token id (matches /^[a-zA-Z0-9_]{1,50}$/).',
|
|
220
|
-
},
|
|
221
|
-
user: { type: 'string', description: 'API user.' },
|
|
222
|
-
},
|
|
223
|
-
required: ['token_id'],
|
|
224
|
-
},
|
|
225
|
-
handler: toolTokenGet,
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: 'thinr_token_create',
|
|
229
|
-
description: `Create a user-level access token and issue a JWT scoped by an \`allow\` (and optional \`deny\`) permission tree.
|
|
230
|
-
|
|
231
|
-
Use this to mint narrow-scope tokens you can hand off to scripts, integrations, or third parties so they can call the platform API on your behalf without your full credentials. The JWT is returned in the response — capture it immediately, it will not be visible from the list endpoint afterwards (only \`thinr_token_get\` shows it).
|
|
232
|
-
|
|
233
|
-
${PERMISSION_GRAMMAR_NOTE}
|
|
234
|
-
|
|
235
|
-
${EXPIRY_NOTE}
|
|
236
|
-
|
|
237
|
-
Example: a backup script that needs to invoke device resources for 90 days.
|
|
238
|
-
{ token_id: "backup_notifier", name: "Backup Notifier", allow: { "Device": { "*": ["AccessDeviceResources"] } }, expire: "90d" }`,
|
|
239
|
-
inputSchema: {
|
|
240
|
-
type: 'object',
|
|
241
|
-
properties: {
|
|
242
|
-
token_id: {
|
|
243
|
-
type: 'string',
|
|
244
|
-
description:
|
|
245
|
-
'Stable token id (letters, digits, underscore, ≤ 50 chars). Used as the URL key for read/update/delete.',
|
|
246
|
-
},
|
|
247
|
-
name: { type: 'string', description: 'Display name.' },
|
|
248
|
-
description: { type: 'string', description: 'Free-form description.' },
|
|
249
|
-
allow: {
|
|
250
|
-
type: 'object',
|
|
251
|
-
description:
|
|
252
|
-
'Permission tree. See the description for the grammar; example: { "Device": { "*": ["AccessDeviceResources"] } }.',
|
|
253
|
-
additionalProperties: true,
|
|
254
|
-
},
|
|
255
|
-
deny: {
|
|
256
|
-
type: 'object',
|
|
257
|
-
description: 'Optional deny tree (same grammar as allow).',
|
|
258
|
-
additionalProperties: true,
|
|
259
|
-
},
|
|
260
|
-
expire: {
|
|
261
|
-
description:
|
|
262
|
-
'Expiry as unix seconds (number) or relative duration ("30d", "12h", "1y"). Omit for no expiry.',
|
|
263
|
-
},
|
|
264
|
-
enabled: {
|
|
265
|
-
type: 'boolean',
|
|
266
|
-
description: 'Default: true. Set to false to mint a token in disabled state.',
|
|
267
|
-
},
|
|
268
|
-
user: { type: 'string', description: 'API user.' },
|
|
269
|
-
},
|
|
270
|
-
required: ['token_id', 'name', 'allow'],
|
|
271
|
-
},
|
|
272
|
-
handler: toolTokenCreate,
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
name: 'thinr_token_update',
|
|
276
|
-
description: `Patch an existing token. Accepted fields per UPDATE_SCHEMA: name, description, enabled, expire, allow, deny. Pass at least one. The JWT itself cannot be rotated through this endpoint — delete and recreate to rotate.
|
|
277
|
-
|
|
278
|
-
${EXPIRY_NOTE}`,
|
|
279
|
-
inputSchema: {
|
|
280
|
-
type: 'object',
|
|
281
|
-
properties: {
|
|
282
|
-
token_id: { type: 'string', description: 'Token id to patch.' },
|
|
283
|
-
name: { type: 'string' },
|
|
284
|
-
description: { type: 'string' },
|
|
285
|
-
enabled: { type: 'boolean' },
|
|
286
|
-
expire: {
|
|
287
|
-
description:
|
|
288
|
-
'Unix seconds (number) or relative duration ("30d", "12h"). Omit to leave unchanged.',
|
|
289
|
-
},
|
|
290
|
-
allow: { type: 'object', additionalProperties: true },
|
|
291
|
-
deny: { type: 'object', additionalProperties: true },
|
|
292
|
-
user: { type: 'string', description: 'API user.' },
|
|
293
|
-
},
|
|
294
|
-
required: ['token_id'],
|
|
295
|
-
},
|
|
296
|
-
handler: toolTokenUpdate,
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
name: 'thinr_token_delete',
|
|
300
|
-
description: `Delete a user-level token. Idempotent — reports cleanly when the token was already absent.`,
|
|
301
|
-
inputSchema: {
|
|
302
|
-
type: 'object',
|
|
303
|
-
properties: {
|
|
304
|
-
token_id: { type: 'string', description: 'Token id to remove.' },
|
|
305
|
-
user: { type: 'string', description: 'API user.' },
|
|
306
|
-
},
|
|
307
|
-
required: ['token_id'],
|
|
308
|
-
},
|
|
309
|
-
handler: toolTokenDelete,
|
|
310
|
-
},
|
|
311
|
-
|
|
312
|
-
// ── Device tokens ──
|
|
313
|
-
{
|
|
314
|
-
name: 'thinr_device_token_list',
|
|
315
|
-
description: `List the device-scoped tokens issued for a single device. Device tokens are a different kind from user-level tokens (\`thinr_token_*\`): they're issued via \`/devices/{id}/tokens\`, scoped optionally to a subset of the device's resources, and have a simpler \`{ token_name, token_resources?, token_expiration? }\` shape.`,
|
|
316
|
-
inputSchema: {
|
|
317
|
-
type: 'object',
|
|
318
|
-
properties: {
|
|
319
|
-
device_id: { type: 'string', description: 'Device ID.' },
|
|
320
|
-
user: { type: 'string', description: 'API user.' },
|
|
321
|
-
},
|
|
322
|
-
required: ['device_id'],
|
|
323
|
-
},
|
|
324
|
-
handler: toolDeviceTokenList,
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
name: 'thinr_device_token_create',
|
|
328
|
-
description: `Issue a device-scoped token. Pass \`token_resources\` to restrict the token to a subset of the device's resources, or omit it for "all resources of this device". Pass \`token_expiration\` (unix seconds or relative like "30d") for an expiry, or omit for none.
|
|
329
|
-
|
|
330
|
-
${EXPIRY_NOTE}
|
|
331
|
-
|
|
332
|
-
Example: a token that lets a remote script call only the \`monitoring\` and \`reboot\` resources of "device-007" for 24 hours.
|
|
333
|
-
{ device: "device-007", token_name: "ops-bot", token_resources: ["monitoring", "reboot"], token_expiration: "24h" }`,
|
|
334
|
-
inputSchema: {
|
|
335
|
-
type: 'object',
|
|
336
|
-
properties: {
|
|
337
|
-
device_id: { type: 'string', description: 'Device ID.' },
|
|
338
|
-
token_name: { type: 'string', description: 'Display name for the new token.' },
|
|
339
|
-
token_resources: {
|
|
340
|
-
type: 'array',
|
|
341
|
-
items: { type: 'string' },
|
|
342
|
-
description:
|
|
343
|
-
'Resource names the token may access. Omit for ALL resources of the device.',
|
|
344
|
-
},
|
|
345
|
-
token_expiration: {
|
|
346
|
-
description:
|
|
347
|
-
'Unix seconds (number) or relative duration ("30d", "24h"). Omit for no expiry.',
|
|
348
|
-
},
|
|
349
|
-
user: { type: 'string', description: 'API user.' },
|
|
350
|
-
},
|
|
351
|
-
required: ['device_id', 'token_name'],
|
|
352
|
-
},
|
|
353
|
-
handler: toolDeviceTokenCreate,
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
name: 'thinr_device_token_delete',
|
|
357
|
-
description: `Delete a device-scoped token. Idempotent.`,
|
|
358
|
-
inputSchema: {
|
|
359
|
-
type: 'object',
|
|
360
|
-
properties: {
|
|
361
|
-
device_id: { type: 'string', description: 'Device ID.' },
|
|
362
|
-
token_id: { type: 'string', description: 'Token id to remove.' },
|
|
363
|
-
user: { type: 'string', description: 'API user.' },
|
|
364
|
-
},
|
|
365
|
-
required: ['device_id', 'token_id'],
|
|
366
|
-
},
|
|
367
|
-
handler: toolDeviceTokenDelete,
|
|
368
|
-
},
|
|
369
|
-
];
|
package/lib/output.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
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/paths.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { resolveUser } from './config.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Account-scoped base paths, one per API version.
|
|
6
|
-
*
|
|
7
|
-
* Every endpoint the CLI talks to hangs off `/v{N}/users/{account}`, and
|
|
8
|
-
* the account is the part that is easy to get wrong: leaving it out still
|
|
9
|
-
* produces a valid URL, one that works against your own account and fails
|
|
10
|
-
* only when acting on behalf of another — with a "not found" that points
|
|
11
|
-
* nowhere near the real problem. Building the prefix in one place keeps
|
|
12
|
-
* the account in the signature, where it is visible, and `tests/paths.test.js`
|
|
13
|
-
* fails the build if a module starts hand-rolling these again.
|
|
14
|
-
*
|
|
15
|
-
* Pass `user` to address another account; omit it for the active profile.
|
|
16
|
-
*/
|
|
17
|
-
export function v1(user) {
|
|
18
|
-
return `/v1/users/${resolveUser(user)}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function v2(user) {
|
|
22
|
-
return `/v2/users/${resolveUser(user)}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function v3(user) {
|
|
26
|
-
return `/v3/users/${resolveUser(user)}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** `/v3/users/{account}/devices/{deviceId}` — the most common prefix. */
|
|
30
|
-
export function device(user, deviceId) {
|
|
31
|
-
return `${v3(user)}/devices/${deviceId}`;
|
|
32
|
-
}
|