@switchbot/openapi-cli 2.6.3 → 2.7.2
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/README.md +2 -2
- package/dist/api/client.js +13 -12
- package/dist/commands/agent-bootstrap.js +21 -15
- package/dist/commands/batch.js +28 -35
- package/dist/commands/capabilities.js +29 -21
- package/dist/commands/catalog.js +12 -3
- package/dist/commands/config.js +32 -38
- package/dist/commands/devices.js +124 -83
- package/dist/commands/doctor.js +355 -19
- package/dist/commands/events.js +112 -23
- package/dist/commands/expand.js +7 -15
- package/dist/commands/explain.js +10 -6
- package/dist/commands/history.js +12 -18
- package/dist/commands/identity.js +59 -0
- package/dist/commands/mcp.js +168 -73
- package/dist/commands/plan.js +1 -1
- package/dist/commands/schema.js +22 -12
- package/dist/commands/watch.js +15 -2
- package/dist/config.js +65 -21
- package/dist/devices/catalog.js +125 -12
- package/dist/devices/resources.js +270 -0
- package/dist/index.js +25 -6
- package/dist/lib/devices.js +22 -7
- package/dist/schema/field-aliases.js +131 -0
- package/dist/utils/filter.js +17 -4
- package/dist/utils/help-json.js +54 -0
- package/dist/utils/output.js +37 -0
- package/package.json +1 -1
package/dist/commands/devices.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
2
|
-
import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError, StructuredUsageError,
|
|
2
|
+
import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError, StructuredUsageError, exitWithError } from '../utils/output.js';
|
|
3
3
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
4
|
-
import { findCatalogEntry, getEffectiveCatalog } from '../devices/catalog.js';
|
|
4
|
+
import { findCatalogEntry, getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
|
|
5
5
|
import { getCachedDevice, loadCache } from '../devices/cache.js';
|
|
6
6
|
import { loadDeviceMeta } from '../devices/device-meta.js';
|
|
7
7
|
import { resolveDeviceId, ALL_STRATEGIES } from '../utils/name-resolver.js';
|
|
@@ -15,6 +15,14 @@ import { registerExpandCommand } from './expand.js';
|
|
|
15
15
|
import { registerDevicesMetaCommand } from './device-meta.js';
|
|
16
16
|
import { isDryRun } from '../utils/flags.js';
|
|
17
17
|
import { DryRunSignal } from '../api/client.js';
|
|
18
|
+
import { resolveField, resolveFieldList, listSupportedFieldInputs } from '../schema/field-aliases.js';
|
|
19
|
+
const EXPAND_HINTS = {
|
|
20
|
+
'Air Conditioner': { command: 'setAll', flags: '--temp 26 --mode cool --fan low --power on' },
|
|
21
|
+
'Curtain': { command: 'setPosition', flags: '--position 50 --mode silent' },
|
|
22
|
+
'Curtain 3': { command: 'setPosition', flags: '--position 50' },
|
|
23
|
+
'Blind Tilt': { command: 'setPosition', flags: '--direction up --angle 50' },
|
|
24
|
+
'Relay Switch 2PM': { command: 'setMode', flags: '--channel 1 --mode edge' },
|
|
25
|
+
};
|
|
18
26
|
export function registerDevicesCommand(program) {
|
|
19
27
|
const COMMAND_TYPES = ['command', 'customize'];
|
|
20
28
|
const devices = program
|
|
@@ -80,7 +88,7 @@ Examples:
|
|
|
80
88
|
`)
|
|
81
89
|
.option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
|
|
82
90
|
.option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
|
|
83
|
-
.option('--filter <expr>', 'Filter devices: comma-separated clauses. Each clause is "key=value" (substring; exact for category), "key!=value" (negated substring), "key~value" (explicit substring), or "key=/regex/" (case-insensitive regex). Supported keys:
|
|
91
|
+
.option('--filter <expr>', 'Filter devices: comma-separated clauses. Each clause is "key=value" (substring; exact for category), "key!=value" (negated substring), "key~value" (explicit substring), or "key=/regex/" (case-insensitive regex). Supported keys: deviceId/id, deviceName/name, deviceType/type, controlType, roomName/room, category, familyName/family, hubDeviceId/hub, roomID/roomid, enableCloudService/cloud, alias.', stringArg('--filter'))
|
|
84
92
|
.action(async (options) => {
|
|
85
93
|
try {
|
|
86
94
|
const body = await fetchDeviceList();
|
|
@@ -90,11 +98,34 @@ Examples:
|
|
|
90
98
|
const hubLocation = buildHubLocationMap(deviceList);
|
|
91
99
|
// Parse --filter into a list of clauses. Shared grammar across
|
|
92
100
|
// `devices list`, `devices batch`, and `events tail` / `mqtt-tail`.
|
|
93
|
-
const LIST_KEYS = ['type', 'name', 'category', 'room'
|
|
101
|
+
const LIST_KEYS = ['deviceId', 'type', 'name', 'category', 'room', 'controlType',
|
|
102
|
+
'family', 'hub', 'roomID', 'cloud', 'alias'];
|
|
103
|
+
const LIST_FILTER_CANONICAL = ['deviceId', 'deviceName', 'deviceType', 'controlType',
|
|
104
|
+
'roomName', 'category', 'familyName', 'hubDeviceId', 'roomID',
|
|
105
|
+
'enableCloudService', 'alias'];
|
|
106
|
+
const LIST_FILTER_TO_RUNTIME = {
|
|
107
|
+
deviceId: 'deviceId',
|
|
108
|
+
deviceName: 'name',
|
|
109
|
+
deviceType: 'type',
|
|
110
|
+
controlType: 'controlType',
|
|
111
|
+
roomName: 'room',
|
|
112
|
+
category: 'category',
|
|
113
|
+
familyName: 'family',
|
|
114
|
+
hubDeviceId: 'hub',
|
|
115
|
+
roomID: 'roomID',
|
|
116
|
+
enableCloudService: 'cloud',
|
|
117
|
+
alias: 'alias',
|
|
118
|
+
};
|
|
94
119
|
let listClauses = null;
|
|
95
120
|
if (options.filter) {
|
|
96
121
|
try {
|
|
97
|
-
listClauses = parseFilterExpr(options.filter, LIST_KEYS
|
|
122
|
+
listClauses = parseFilterExpr(options.filter, LIST_KEYS, {
|
|
123
|
+
resolveKey: (input) => {
|
|
124
|
+
const canonical = resolveField(input, LIST_FILTER_CANONICAL);
|
|
125
|
+
return LIST_FILTER_TO_RUNTIME[canonical];
|
|
126
|
+
},
|
|
127
|
+
supportedKeys: listSupportedFieldInputs(LIST_FILTER_CANONICAL),
|
|
128
|
+
});
|
|
98
129
|
}
|
|
99
130
|
catch (err) {
|
|
100
131
|
if (err instanceof FilterSyntaxError)
|
|
@@ -114,10 +145,10 @@ Examples:
|
|
|
114
145
|
};
|
|
115
146
|
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
116
147
|
if (listClauses) {
|
|
117
|
-
const filteredDeviceList = deviceList.filter((d) => matchesFilter({ type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '' }));
|
|
148
|
+
const filteredDeviceList = deviceList.filter((d) => matchesFilter({ deviceId: d.deviceId, type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '', controlType: d.controlType || '', family: d.familyName || '', hub: d.hubDeviceId || '', roomID: d.roomID || '', cloud: String(d.enableCloudService), alias: deviceMeta.devices[d.deviceId]?.alias || '' }));
|
|
118
149
|
const filteredIrList = infraredRemoteList.filter((d) => {
|
|
119
150
|
const inherited = hubLocation.get(d.hubDeviceId);
|
|
120
|
-
return matchesFilter({ type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '' });
|
|
151
|
+
return matchesFilter({ deviceId: d.deviceId, type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '', controlType: d.controlType || '', family: inherited?.family || '', hub: d.hubDeviceId || '', roomID: inherited?.roomID || '', cloud: '', alias: deviceMeta.devices[d.deviceId]?.alias || '' });
|
|
121
152
|
});
|
|
122
153
|
printJson({ ok: true, deviceList: filteredDeviceList, infraredRemoteList: filteredIrList });
|
|
123
154
|
}
|
|
@@ -134,7 +165,7 @@ Examples:
|
|
|
134
165
|
for (const d of deviceList) {
|
|
135
166
|
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
136
167
|
continue;
|
|
137
|
-
if (!matchesFilter({ type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '' }))
|
|
168
|
+
if (!matchesFilter({ deviceId: d.deviceId, type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '', controlType: d.controlType || '', family: d.familyName || '', hub: d.hubDeviceId || '', roomID: d.roomID || '', cloud: String(d.enableCloudService), alias: deviceMeta.devices[d.deviceId]?.alias || '' }))
|
|
138
169
|
continue;
|
|
139
170
|
rows.push([
|
|
140
171
|
d.deviceId,
|
|
@@ -154,7 +185,7 @@ Examples:
|
|
|
154
185
|
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
155
186
|
continue;
|
|
156
187
|
const inherited = hubLocation.get(d.hubDeviceId);
|
|
157
|
-
if (!matchesFilter({ type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '' }))
|
|
188
|
+
if (!matchesFilter({ deviceId: d.deviceId, type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '', controlType: d.controlType || '', family: inherited?.family || '', hub: d.hubDeviceId || '', roomID: inherited?.roomID || '', cloud: '', alias: deviceMeta.devices[d.deviceId]?.alias || '' }))
|
|
158
189
|
continue;
|
|
159
190
|
rows.push([
|
|
160
191
|
d.deviceId,
|
|
@@ -177,9 +208,19 @@ Examples:
|
|
|
177
208
|
const defaultFields = options.wide ? undefined : narrowHeaders;
|
|
178
209
|
// Accept API field names and short aliases alongside canonical column names
|
|
179
210
|
const DEVICE_LIST_ALIASES = {
|
|
180
|
-
id: 'deviceId',
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
id: 'deviceId',
|
|
212
|
+
name: 'deviceName',
|
|
213
|
+
deviceType: 'type',
|
|
214
|
+
type: 'type',
|
|
215
|
+
roomName: 'room',
|
|
216
|
+
familyName: 'family',
|
|
217
|
+
hubDeviceId: 'hub',
|
|
218
|
+
enableCloudService: 'cloud',
|
|
219
|
+
controlType: 'controlType',
|
|
220
|
+
deviceName: 'deviceName',
|
|
221
|
+
deviceId: 'deviceId',
|
|
222
|
+
category: 'category',
|
|
223
|
+
alias: 'alias',
|
|
183
224
|
};
|
|
184
225
|
renderRows(wideHeaders, rows, fmt, userFields ?? defaultFields, DEVICE_LIST_ALIASES);
|
|
185
226
|
if (fmt === 'table') {
|
|
@@ -247,7 +288,7 @@ Examples:
|
|
|
247
288
|
}
|
|
248
289
|
}
|
|
249
290
|
else {
|
|
250
|
-
const
|
|
291
|
+
const rawFields = resolveFields();
|
|
251
292
|
for (const entry of batch) {
|
|
252
293
|
const { deviceId, ok, error, _fetchedAt: ts, ...status } = entry;
|
|
253
294
|
console.log(`\n─── ${String(deviceId)} ───`);
|
|
@@ -255,9 +296,13 @@ Examples:
|
|
|
255
296
|
console.error(` error: ${String(error)}`);
|
|
256
297
|
}
|
|
257
298
|
else {
|
|
299
|
+
const statusMap = status;
|
|
300
|
+
const fields = rawFields
|
|
301
|
+
? resolveFieldList(rawFields, Object.keys(statusMap))
|
|
302
|
+
: undefined;
|
|
258
303
|
const displayStatus = fields
|
|
259
|
-
? Object.fromEntries(fields.map((f) => [f,
|
|
260
|
-
:
|
|
304
|
+
? Object.fromEntries(fields.map((f) => [f, statusMap[f] ?? null]))
|
|
305
|
+
: statusMap;
|
|
261
306
|
printKeyValue(displayStatus);
|
|
262
307
|
console.error(` fetched at ${String(ts)}`);
|
|
263
308
|
}
|
|
@@ -282,7 +327,10 @@ Examples:
|
|
|
282
327
|
const statusWithTs = { ...body, _fetchedAt: fetchedAt };
|
|
283
328
|
const allHeaders = Object.keys(statusWithTs);
|
|
284
329
|
const allRows = [Object.values(statusWithTs)];
|
|
285
|
-
const
|
|
330
|
+
const rawFields = resolveFields();
|
|
331
|
+
const fields = rawFields
|
|
332
|
+
? resolveFieldList(rawFields, allHeaders)
|
|
333
|
+
: undefined;
|
|
286
334
|
renderRows(allHeaders, allRows, fmt, fields);
|
|
287
335
|
return;
|
|
288
336
|
}
|
|
@@ -420,26 +468,22 @@ Examples:
|
|
|
420
468
|
const validation = validateCommand(deviceId, cmd, parameter, options.type);
|
|
421
469
|
if (!validation.ok) {
|
|
422
470
|
const err = validation.error;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
else {
|
|
431
|
-
console.error(`Error: ${err.message}`);
|
|
432
|
-
if (err.hint)
|
|
433
|
-
console.error(err.hint);
|
|
434
|
-
if (err.kind === 'unknown-command') {
|
|
435
|
-
const cached = getCachedDevice(deviceId);
|
|
436
|
-
if (cached) {
|
|
437
|
-
console.error(`Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.`);
|
|
438
|
-
console.error(`(If the catalog is out of date, run 'switchbot devices list' to refresh the local cache, or pass --type customize for custom IR buttons.)`);
|
|
439
|
-
}
|
|
471
|
+
let hint = err.hint;
|
|
472
|
+
if (err.kind === 'unknown-command') {
|
|
473
|
+
const cached = getCachedDevice(deviceId);
|
|
474
|
+
if (cached) {
|
|
475
|
+
const extra = `Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.\n` +
|
|
476
|
+
`(If the catalog is out of date, run 'switchbot devices list' to refresh the local cache, or pass --type customize for custom IR buttons.)`;
|
|
477
|
+
hint = hint ? `${hint}\n${extra}` : extra;
|
|
440
478
|
}
|
|
441
479
|
}
|
|
442
|
-
|
|
480
|
+
exitWithError({
|
|
481
|
+
code: 2,
|
|
482
|
+
kind: 'usage',
|
|
483
|
+
message: err.message,
|
|
484
|
+
hint,
|
|
485
|
+
context: { validationKind: err.kind },
|
|
486
|
+
});
|
|
443
487
|
}
|
|
444
488
|
// Case-only mismatch: emit a warning and continue with the canonical name.
|
|
445
489
|
if (validation.caseNormalizedFrom && validation.normalized) {
|
|
@@ -454,18 +498,10 @@ Examples:
|
|
|
454
498
|
if (cachedForParam && options.type === 'command' && !options.skipParamValidation) {
|
|
455
499
|
const paramCheck = validateParameter(cachedForParam.type, cmd, parameter);
|
|
456
500
|
if (!paramCheck.ok) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
message: paramCheck.error,
|
|
462
|
-
context: { command: cmd, deviceType: cachedForParam.type, deviceId, humanHint: paramCheck.error },
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
console.error(`Error: ${paramCheck.error}`);
|
|
467
|
-
}
|
|
468
|
-
process.exit(2);
|
|
501
|
+
exitWithError({
|
|
502
|
+
message: `Error: ${paramCheck.error}`,
|
|
503
|
+
context: { command: cmd, deviceType: cachedForParam.type, deviceId, humanHint: paramCheck.error },
|
|
504
|
+
});
|
|
469
505
|
}
|
|
470
506
|
if (paramCheck.normalized !== undefined)
|
|
471
507
|
parameter = paramCheck.normalized;
|
|
@@ -476,24 +512,14 @@ Examples:
|
|
|
476
512
|
isDestructiveCommand(cachedForGuard?.type, cmd, options.type)) {
|
|
477
513
|
const typeLabel = cachedForGuard?.type ?? 'unknown';
|
|
478
514
|
const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
context: { command: cmd, deviceType: typeLabel, deviceId, ...(reason ? { destructiveReason: reason } : {}) },
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
console.error(`Refusing to run destructive command "${cmd}" on ${typeLabel} without --yes.`);
|
|
492
|
-
if (reason)
|
|
493
|
-
console.error(`Reason: ${reason}`);
|
|
494
|
-
console.error(`Re-run with --yes to confirm, or --dry-run to preview without sending.`);
|
|
495
|
-
}
|
|
496
|
-
process.exit(2);
|
|
515
|
+
exitWithError({
|
|
516
|
+
kind: 'guard',
|
|
517
|
+
message: `Refusing to run destructive command "${cmd}" on ${typeLabel} without --yes.`,
|
|
518
|
+
hint: reason
|
|
519
|
+
? `Re-run with --yes to confirm. Reason: ${reason}`
|
|
520
|
+
: 'Re-run with --yes to confirm, or --dry-run to preview without sending.',
|
|
521
|
+
context: { command: cmd, deviceType: typeLabel, deviceId, ...(reason ? { safetyReason: reason, destructiveReason: reason } : {}) },
|
|
522
|
+
});
|
|
497
523
|
}
|
|
498
524
|
// Warn when --yes is given but the command is not destructive (no-op flag)
|
|
499
525
|
if (options.yes && !isDestructiveCommand(cachedForGuard?.type, cmd, options.type) && !isDryRun()) {
|
|
@@ -630,7 +656,7 @@ Examples:
|
|
|
630
656
|
const joinedMatch = findCatalogEntry(joined);
|
|
631
657
|
if (joinedMatch && !Array.isArray(joinedMatch)) {
|
|
632
658
|
if (isJsonMode()) {
|
|
633
|
-
printJson(joinedMatch);
|
|
659
|
+
printJson(normalizeCatalogForJson(joinedMatch));
|
|
634
660
|
}
|
|
635
661
|
else {
|
|
636
662
|
renderCatalogEntry(joinedMatch);
|
|
@@ -649,7 +675,7 @@ Examples:
|
|
|
649
675
|
}
|
|
650
676
|
if (individualMatches.length === typeParts.length) {
|
|
651
677
|
if (isJsonMode()) {
|
|
652
|
-
printJson(individualMatches);
|
|
678
|
+
printJson(individualMatches.map(normalizeCatalogForJson));
|
|
653
679
|
}
|
|
654
680
|
else {
|
|
655
681
|
individualMatches.forEach((entry, i) => {
|
|
@@ -702,7 +728,8 @@ JSON output shape (--json):
|
|
|
702
728
|
liveStatus: <status payload when --live was passed>
|
|
703
729
|
},
|
|
704
730
|
source: "catalog" | "live" | "catalog+live" | "none",
|
|
705
|
-
suggestedActions: [{command, parameter?, description}]
|
|
731
|
+
suggestedActions: [{command, parameter?, description}],
|
|
732
|
+
expandHint?: {command, flags, example} // present when the type supports 'devices expand'
|
|
706
733
|
}
|
|
707
734
|
|
|
708
735
|
Examples:
|
|
@@ -722,6 +749,7 @@ Examples:
|
|
|
722
749
|
const result = await describeDevice(deviceId, options);
|
|
723
750
|
const { device, isPhysical, typeName, controlType, catalog, capabilities, source, suggestedActions: picks } = result;
|
|
724
751
|
if (isJsonMode()) {
|
|
752
|
+
const expandHint = catalog ? EXPAND_HINTS[catalog.type] : undefined;
|
|
725
753
|
printJson({
|
|
726
754
|
device,
|
|
727
755
|
controlType,
|
|
@@ -729,6 +757,7 @@ Examples:
|
|
|
729
757
|
capabilities,
|
|
730
758
|
source,
|
|
731
759
|
suggestedActions: picks,
|
|
760
|
+
...(expandHint ? { expandHint: { command: expandHint.command, flags: expandHint.flags, example: `switchbot devices expand ${deviceId} ${expandHint.command} ${expandHint.flags}` } } : {}),
|
|
732
761
|
});
|
|
733
762
|
return;
|
|
734
763
|
}
|
|
@@ -785,20 +814,12 @@ Examples:
|
|
|
785
814
|
catch (error) {
|
|
786
815
|
if (error instanceof DeviceNotFoundError) {
|
|
787
816
|
const message = `${error.message} Try 'switchbot devices list' to see the full list.`;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
transient: false,
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
console.error(error.message);
|
|
799
|
-
console.error(`Try 'switchbot devices list' to see the full list.`);
|
|
800
|
-
}
|
|
801
|
-
process.exit(1);
|
|
817
|
+
exitWithError({
|
|
818
|
+
code: 1,
|
|
819
|
+
kind: 'runtime',
|
|
820
|
+
message,
|
|
821
|
+
extra: { errorClass: 'runtime', transient: false },
|
|
822
|
+
});
|
|
802
823
|
}
|
|
803
824
|
handleError(error);
|
|
804
825
|
}
|
|
@@ -814,6 +835,21 @@ Examples:
|
|
|
814
835
|
// switchbot devices meta set/get/list/clear
|
|
815
836
|
registerDevicesMetaCommand(devices);
|
|
816
837
|
}
|
|
838
|
+
function normalizeCatalogForJson(entry) {
|
|
839
|
+
return {
|
|
840
|
+
...entry,
|
|
841
|
+
commands: entry.commands.map((c) => {
|
|
842
|
+
const tier = deriveSafetyTier(c, entry);
|
|
843
|
+
const reason = getCommandSafetyReason(c);
|
|
844
|
+
return {
|
|
845
|
+
...c,
|
|
846
|
+
safetyTier: tier,
|
|
847
|
+
destructive: tier === 'destructive',
|
|
848
|
+
...(reason ? { safetyReason: reason } : {}),
|
|
849
|
+
};
|
|
850
|
+
}),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
817
853
|
function renderCatalogEntry(entry) {
|
|
818
854
|
console.log(`Type: ${entry.type}`);
|
|
819
855
|
console.log(`Category: ${entry.category === 'ir' ? 'IR remote' : 'Physical device'}`);
|
|
@@ -831,10 +867,11 @@ function renderCatalogEntry(entry) {
|
|
|
831
867
|
console.log('\nCommands:');
|
|
832
868
|
const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
|
|
833
869
|
const rows = entry.commands.map((c) => {
|
|
870
|
+
const tier = deriveSafetyTier(c, entry);
|
|
834
871
|
const flags = [];
|
|
835
872
|
if (c.commandType === 'customize')
|
|
836
873
|
flags.push('customize');
|
|
837
|
-
if (
|
|
874
|
+
if (tier === 'destructive')
|
|
838
875
|
flags.push('!destructive');
|
|
839
876
|
const label = flags.length > 0 ? `${c.command} [${flags.join(', ')}]` : c.command;
|
|
840
877
|
const base = [label, c.parameter, c.description];
|
|
@@ -844,7 +881,7 @@ function renderCatalogEntry(entry) {
|
|
|
844
881
|
? ['command', 'parameter', 'description', 'example']
|
|
845
882
|
: ['command', 'parameter', 'description'];
|
|
846
883
|
printTable(tableHeaders, rows);
|
|
847
|
-
const hasDestructive = entry.commands.some((c) => c
|
|
884
|
+
const hasDestructive = entry.commands.some((c) => deriveSafetyTier(c, entry) === 'destructive');
|
|
848
885
|
if (hasDestructive) {
|
|
849
886
|
console.log('\n[!destructive] commands have hard-to-reverse real-world effects — confirm before issuing.');
|
|
850
887
|
}
|
|
@@ -853,4 +890,8 @@ function renderCatalogEntry(entry) {
|
|
|
853
890
|
console.log('\nStatus fields (from "devices status"):');
|
|
854
891
|
console.log(' ' + entry.statusFields.join(', '));
|
|
855
892
|
}
|
|
893
|
+
const expandHint = EXPAND_HINTS[entry.type];
|
|
894
|
+
if (expandHint) {
|
|
895
|
+
console.log(`\nTip: Use 'devices expand <id> ${expandHint.command} ${expandHint.flags}' for semantic flags instead of raw parameters.`);
|
|
896
|
+
}
|
|
856
897
|
}
|