@switchbot/openapi-cli 2.1.0 → 2.2.1
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 +58 -3
- package/dist/commands/batch.js +7 -5
- package/dist/commands/cache.js +3 -1
- package/dist/commands/capabilities.js +30 -20
- package/dist/commands/catalog.js +3 -1
- package/dist/commands/completion.js +139 -12
- package/dist/commands/config.js +4 -3
- package/dist/commands/device-meta.js +3 -2
- package/dist/commands/devices.js +217 -71
- package/dist/commands/events.js +133 -15
- package/dist/commands/expand.js +29 -11
- package/dist/commands/history.js +4 -3
- package/dist/commands/mcp.js +41 -5
- package/dist/commands/plan.js +10 -2
- package/dist/commands/scenes.js +1 -1
- package/dist/commands/schema.js +6 -3
- package/dist/commands/watch.js +16 -4
- package/dist/commands/webhook.js +2 -1
- package/dist/config.js +7 -2
- package/dist/index.js +49 -19
- package/dist/lib/devices.js +16 -1
- package/dist/mcp/device-history.js +66 -0
- package/dist/mcp/events-subscription.js +10 -3
- package/dist/mqtt/client.js +8 -0
- package/dist/mqtt/credential.js +3 -2
- package/dist/sinks/dispatcher.js +12 -0
- package/dist/sinks/file.js +19 -0
- package/dist/sinks/format.js +56 -0
- package/dist/sinks/homeassistant.js +44 -0
- package/dist/sinks/openclaw.js +33 -0
- package/dist/sinks/stdout.js +5 -0
- package/dist/sinks/telegram.js +28 -0
- package/dist/sinks/types.js +1 -0
- package/dist/sinks/webhook.js +22 -0
- package/dist/utils/arg-parsers.js +62 -0
- package/dist/utils/flags.js +13 -12
- package/dist/utils/format.js +6 -5
- package/package.json +1 -1
package/dist/commands/devices.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
3
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
4
|
import { findCatalogEntry, getEffectiveCatalog } from '../devices/catalog.js';
|
|
@@ -12,6 +13,7 @@ import { registerExpandCommand } from './expand.js';
|
|
|
12
13
|
import { registerDevicesMetaCommand } from './device-meta.js';
|
|
13
14
|
import { isDryRun } from '../utils/flags.js';
|
|
14
15
|
export function registerDevicesCommand(program) {
|
|
16
|
+
const COMMAND_TYPES = ['command', 'customize'];
|
|
15
17
|
const devices = program
|
|
16
18
|
.command('devices')
|
|
17
19
|
.description('Manage and control SwitchBot devices')
|
|
@@ -38,6 +40,7 @@ Run any subcommand with --help for its own flags and examples.
|
|
|
38
40
|
// switchbot devices list
|
|
39
41
|
devices
|
|
40
42
|
.command('list')
|
|
43
|
+
.alias('ls')
|
|
41
44
|
.description('List all physical devices and IR remote devices on the account')
|
|
42
45
|
.addHelpText('after', `
|
|
43
46
|
Default columns: deviceId, deviceName, type, category
|
|
@@ -66,20 +69,62 @@ Examples:
|
|
|
66
69
|
$ switchbot devices list --format tsv --fields deviceId,deviceName,type,category
|
|
67
70
|
$ switchbot devices list --json | jq '.deviceList[] | select(.familyName == "家里")'
|
|
68
71
|
$ switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
|
|
72
|
+
$ switchbot devices list --filter type="Air Conditioner"
|
|
73
|
+
$ switchbot devices list --filter category=ir
|
|
74
|
+
$ switchbot devices list --filter name=living,category=physical
|
|
69
75
|
`)
|
|
70
76
|
.option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
|
|
71
77
|
.option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
|
|
78
|
+
.option('--filter <expr>', 'Filter devices: "type=X", "name=X", "category=physical|ir", "room=X" (comma-separated key=value pairs)', stringArg('--filter'))
|
|
72
79
|
.action(async (options) => {
|
|
73
80
|
try {
|
|
74
81
|
const body = await fetchDeviceList();
|
|
75
82
|
const { deviceList, infraredRemoteList } = body;
|
|
76
83
|
const fmt = resolveFormat();
|
|
77
84
|
const deviceMeta = loadDeviceMeta();
|
|
85
|
+
const hubLocation = buildHubLocationMap(deviceList);
|
|
86
|
+
let listFilter = null;
|
|
87
|
+
if (options.filter) {
|
|
88
|
+
listFilter = {};
|
|
89
|
+
for (const pair of options.filter.split(',')) {
|
|
90
|
+
const eq = pair.indexOf('=');
|
|
91
|
+
if (eq === -1)
|
|
92
|
+
throw new UsageError(`Invalid --filter pair "${pair.trim()}". Expected key=value.`);
|
|
93
|
+
const k = pair.slice(0, eq).trim();
|
|
94
|
+
const v = pair.slice(eq + 1).trim();
|
|
95
|
+
if (!['type', 'name', 'category', 'room'].includes(k)) {
|
|
96
|
+
throw new UsageError(`Unknown --filter key "${k}". Supported: type, name, category, room.`);
|
|
97
|
+
}
|
|
98
|
+
listFilter[k] = v.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const matchesFilter = (entry) => {
|
|
102
|
+
if (!listFilter)
|
|
103
|
+
return true;
|
|
104
|
+
if (listFilter.type && !entry.type.toLowerCase().includes(listFilter.type))
|
|
105
|
+
return false;
|
|
106
|
+
if (listFilter.name && !entry.name.toLowerCase().includes(listFilter.name))
|
|
107
|
+
return false;
|
|
108
|
+
if (listFilter.category && entry.category !== listFilter.category)
|
|
109
|
+
return false;
|
|
110
|
+
if (listFilter.room && !entry.room.toLowerCase().includes(listFilter.room))
|
|
111
|
+
return false;
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
78
114
|
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
79
|
-
|
|
115
|
+
if (listFilter) {
|
|
116
|
+
const filteredDeviceList = deviceList.filter((d) => matchesFilter({ type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '' }));
|
|
117
|
+
const filteredIrList = infraredRemoteList.filter((d) => {
|
|
118
|
+
const inherited = hubLocation.get(d.hubDeviceId);
|
|
119
|
+
return matchesFilter({ type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '' });
|
|
120
|
+
});
|
|
121
|
+
printJson({ ok: true, deviceList: filteredDeviceList, infraredRemoteList: filteredIrList });
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
printJson({ ok: true, ...body });
|
|
125
|
+
}
|
|
80
126
|
return;
|
|
81
127
|
}
|
|
82
|
-
const hubLocation = buildHubLocationMap(deviceList);
|
|
83
128
|
const narrowHeaders = ['deviceId', 'deviceName', 'type', 'category'];
|
|
84
129
|
const wideHeaders = ['deviceId', 'deviceName', 'type', 'category', 'controlType', 'family', 'roomID', 'room', 'hub', 'cloud', 'alias'];
|
|
85
130
|
const userFields = resolveFields();
|
|
@@ -88,6 +133,8 @@ Examples:
|
|
|
88
133
|
for (const d of deviceList) {
|
|
89
134
|
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
90
135
|
continue;
|
|
136
|
+
if (!matchesFilter({ type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '' }))
|
|
137
|
+
continue;
|
|
91
138
|
rows.push([
|
|
92
139
|
d.deviceId,
|
|
93
140
|
d.deviceName,
|
|
@@ -106,6 +153,8 @@ Examples:
|
|
|
106
153
|
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
107
154
|
continue;
|
|
108
155
|
const inherited = hubLocation.get(d.hubDeviceId);
|
|
156
|
+
if (!matchesFilter({ type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '' }))
|
|
157
|
+
continue;
|
|
109
158
|
rows.push([
|
|
110
159
|
d.deviceId,
|
|
111
160
|
d.deviceName,
|
|
@@ -121,13 +170,22 @@ Examples:
|
|
|
121
170
|
]);
|
|
122
171
|
}
|
|
123
172
|
if (rows.length === 0 && fmt === 'table') {
|
|
124
|
-
console.log('No devices found');
|
|
173
|
+
console.log(listFilter ? 'No devices matched the filter.' : 'No devices found');
|
|
125
174
|
return;
|
|
126
175
|
}
|
|
127
176
|
const defaultFields = options.wide ? undefined : narrowHeaders;
|
|
128
|
-
|
|
177
|
+
// Accept API field names and short aliases alongside canonical column names
|
|
178
|
+
const DEVICE_LIST_ALIASES = {
|
|
179
|
+
name: 'deviceName', deviceType: 'type', type: 'type',
|
|
180
|
+
roomName: 'room', familyName: 'family',
|
|
181
|
+
hubDeviceId: 'hub', enableCloudService: 'cloud',
|
|
182
|
+
};
|
|
183
|
+
renderRows(wideHeaders, rows, fmt, userFields ?? defaultFields, DEVICE_LIST_ALIASES);
|
|
129
184
|
if (fmt === 'table') {
|
|
130
|
-
|
|
185
|
+
const totalLabel = listFilter
|
|
186
|
+
? `${rows.length} match(es) (${deviceList.length} physical + ${infraredRemoteList.length} IR before filter)`
|
|
187
|
+
: `${deviceList.length} physical device(s), ${infraredRemoteList.length} IR remote device(s)`;
|
|
188
|
+
console.log(`\nTotal: ${totalLabel}`);
|
|
131
189
|
console.log(`Tip: 'switchbot devices describe <deviceId>' shows a device's supported commands.`);
|
|
132
190
|
}
|
|
133
191
|
}
|
|
@@ -139,8 +197,9 @@ Examples:
|
|
|
139
197
|
devices
|
|
140
198
|
.command('status')
|
|
141
199
|
.description('Query the real-time status of a specific device')
|
|
142
|
-
.argument('[deviceId]', 'Device ID from "devices list" (or use --name)')
|
|
143
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
200
|
+
.argument('[deviceId]', 'Device ID from "devices list" (or use --name or --ids)')
|
|
201
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
202
|
+
.option('--ids <list>', 'Comma-separated device IDs for batch status (incompatible with --name)', stringArg('--ids'))
|
|
144
203
|
.addHelpText('after', `
|
|
145
204
|
Status fields vary by device type. To discover them without a live call:
|
|
146
205
|
|
|
@@ -155,25 +214,70 @@ Examples:
|
|
|
155
214
|
$ switchbot devices status ABC123DEF456 --json
|
|
156
215
|
$ switchbot devices status ABC123DEF456 --format yaml
|
|
157
216
|
$ switchbot devices status ABC123DEF456 --format tsv --fields power,battery
|
|
158
|
-
$ switchbot devices status ABC123DEF456 --json | jq '.battery'
|
|
217
|
+
$ switchbot devices status ABC123DEF456 --json | jq '.data.battery'
|
|
218
|
+
$ switchbot devices status --ids ABC123,DEF456,GHI789
|
|
219
|
+
$ switchbot devices status --ids ABC123,DEF456 --fields power,battery
|
|
159
220
|
`)
|
|
160
221
|
.action(async (deviceIdArg, options) => {
|
|
161
222
|
try {
|
|
223
|
+
// Batch mode: --ids id1,id2,id3
|
|
224
|
+
if (options.ids) {
|
|
225
|
+
if (options.name)
|
|
226
|
+
throw new UsageError('--ids and --name cannot be used together.');
|
|
227
|
+
const ids = options.ids.split(',').map((s) => s.trim()).filter(Boolean);
|
|
228
|
+
if (ids.length === 0)
|
|
229
|
+
throw new UsageError('--ids requires at least one device ID.');
|
|
230
|
+
const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
|
|
231
|
+
const fetchedAt = new Date().toISOString();
|
|
232
|
+
const batch = results.map((r, i) => r.status === 'fulfilled'
|
|
233
|
+
? { deviceId: ids[i], ok: true, _fetchedAt: fetchedAt, ...r.value }
|
|
234
|
+
: { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) });
|
|
235
|
+
const batchFmt = resolveFormat();
|
|
236
|
+
if (isJsonMode() || batchFmt === 'json') {
|
|
237
|
+
printJson(batch);
|
|
238
|
+
}
|
|
239
|
+
else if (batchFmt === 'jsonl') {
|
|
240
|
+
for (const entry of batch) {
|
|
241
|
+
console.log(JSON.stringify(entry));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const fields = resolveFields();
|
|
246
|
+
for (const entry of batch) {
|
|
247
|
+
const { deviceId, ok, error, _fetchedAt: ts, ...status } = entry;
|
|
248
|
+
console.log(`\n─── ${String(deviceId)} ───`);
|
|
249
|
+
if (!ok) {
|
|
250
|
+
console.error(` error: ${String(error)}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const displayStatus = fields
|
|
254
|
+
? Object.fromEntries(fields.map((f) => [f, status[f] ?? null]))
|
|
255
|
+
: status;
|
|
256
|
+
printKeyValue(displayStatus);
|
|
257
|
+
console.error(` fetched at ${String(ts)}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
162
263
|
const deviceId = resolveDeviceId(deviceIdArg, options.name);
|
|
163
264
|
const body = await fetchDeviceStatus(deviceId);
|
|
265
|
+
const fetchedAt = new Date().toISOString();
|
|
164
266
|
const fmt = resolveFormat();
|
|
165
267
|
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
166
|
-
printJson(body);
|
|
268
|
+
printJson({ ...body, _fetchedAt: fetchedAt });
|
|
167
269
|
return;
|
|
168
270
|
}
|
|
169
271
|
if (fmt !== 'table') {
|
|
170
|
-
const
|
|
171
|
-
const
|
|
272
|
+
const statusWithTs = { ...body, _fetchedAt: fetchedAt };
|
|
273
|
+
const allHeaders = Object.keys(statusWithTs);
|
|
274
|
+
const allRows = [Object.values(statusWithTs)];
|
|
172
275
|
const fields = resolveFields();
|
|
173
276
|
renderRows(allHeaders, allRows, fmt, fields);
|
|
174
277
|
return;
|
|
175
278
|
}
|
|
176
279
|
printKeyValue(body);
|
|
280
|
+
console.error(`\nfetched at ${fetchedAt}`);
|
|
177
281
|
}
|
|
178
282
|
catch (error) {
|
|
179
283
|
handleError(error);
|
|
@@ -184,12 +288,12 @@ Examples:
|
|
|
184
288
|
.command('command')
|
|
185
289
|
.description('Send a control command to a device')
|
|
186
290
|
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
187
|
-
.argument('
|
|
291
|
+
.argument('[cmd]', 'Command name, e.g. turnOn, turnOff, setColor, setBrightness, setAll, startClean')
|
|
188
292
|
.argument('[parameter]', 'Command parameter. Omit for commands like turnOn/turnOff (defaults to "default"). Format depends on the command (see below).')
|
|
189
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
190
|
-
.option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', 'command')
|
|
293
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
294
|
+
.option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
|
|
191
295
|
.option('--yes', 'Confirm a destructive command (Smart Lock unlock, Garage open, …). --dry-run is always allowed without --yes.')
|
|
192
|
-
.option('--idempotency-key <key>', 'Idempotency key for deduplication (60s window; same key replays cached result)')
|
|
296
|
+
.option('--idempotency-key <key>', 'Idempotency key for deduplication (60s window; same key replays cached result)', stringArg('--idempotency-key'))
|
|
193
297
|
.addHelpText('after', `
|
|
194
298
|
────────────────────────────────────────────────────────────────────────
|
|
195
299
|
For the full list of commands a specific device supports — and their
|
|
@@ -235,60 +339,89 @@ Examples:
|
|
|
235
339
|
$ switchbot devices command ABC123 "MyButton" --type customize
|
|
236
340
|
$ switchbot devices command <lockId> unlock --yes
|
|
237
341
|
`)
|
|
238
|
-
.action(async (deviceIdArg,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
342
|
+
.action(async (deviceIdArg, cmdArg, parameter, options) => {
|
|
343
|
+
try {
|
|
344
|
+
// BUG-FIX: When --name is provided, Commander misassigns the first positional
|
|
345
|
+
// to [deviceId] instead of [cmd]. Detect and shift positionals accordingly.
|
|
346
|
+
let cmd;
|
|
347
|
+
let effectiveDeviceIdArg;
|
|
348
|
+
if (options.name) {
|
|
349
|
+
if (deviceIdArg && cmdArg) {
|
|
350
|
+
throw new UsageError('Provide either a deviceId argument or --name, not both.');
|
|
351
|
+
}
|
|
352
|
+
if (!deviceIdArg && !cmdArg) {
|
|
353
|
+
throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
|
|
354
|
+
}
|
|
355
|
+
// --name "x" turnOn → deviceIdArg="turnOn", cmdArg=undefined → shift
|
|
356
|
+
cmd = (deviceIdArg ?? cmdArg);
|
|
357
|
+
effectiveDeviceIdArg = undefined;
|
|
249
358
|
}
|
|
250
359
|
else {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
360
|
+
if (!cmdArg) {
|
|
361
|
+
throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
|
|
362
|
+
}
|
|
363
|
+
cmd = cmdArg;
|
|
364
|
+
effectiveDeviceIdArg = deviceIdArg;
|
|
365
|
+
}
|
|
366
|
+
const deviceId = resolveDeviceId(effectiveDeviceIdArg, options.name);
|
|
367
|
+
if (!getCachedDevice(deviceId)) {
|
|
368
|
+
console.error(`Note: device ${deviceId} is not in the local cache — run 'switchbot devices list' first to enable command validation.`);
|
|
369
|
+
}
|
|
370
|
+
const validation = validateCommand(deviceId, cmd, parameter, options.type);
|
|
371
|
+
if (!validation.ok) {
|
|
372
|
+
const err = validation.error;
|
|
373
|
+
if (isJsonMode()) {
|
|
374
|
+
const obj = { code: 2, kind: 'usage', message: err.message };
|
|
375
|
+
if (err.hint)
|
|
376
|
+
obj.hint = err.hint;
|
|
377
|
+
obj.context = { validationKind: err.kind };
|
|
378
|
+
console.error(JSON.stringify({ error: obj }));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
console.error(`Error: ${err.message}`);
|
|
382
|
+
if (err.hint)
|
|
383
|
+
console.error(err.hint);
|
|
384
|
+
if (err.kind === 'unknown-command') {
|
|
385
|
+
const cached = getCachedDevice(deviceId);
|
|
386
|
+
if (cached) {
|
|
387
|
+
console.error(`Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.`);
|
|
388
|
+
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.)`);
|
|
389
|
+
}
|
|
259
390
|
}
|
|
260
391
|
}
|
|
392
|
+
process.exit(2);
|
|
261
393
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
394
|
+
const cachedForGuard = getCachedDevice(deviceId);
|
|
395
|
+
if (!options.yes &&
|
|
396
|
+
!isDryRun() &&
|
|
397
|
+
isDestructiveCommand(cachedForGuard?.type, cmd, options.type)) {
|
|
398
|
+
const typeLabel = cachedForGuard?.type ?? 'unknown';
|
|
399
|
+
const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
|
|
400
|
+
if (isJsonMode()) {
|
|
401
|
+
console.error(JSON.stringify({
|
|
402
|
+
error: {
|
|
403
|
+
code: 2,
|
|
404
|
+
kind: 'guard',
|
|
405
|
+
message: `"${cmd}" on ${typeLabel} is destructive and requires --yes.`,
|
|
406
|
+
hint: reason
|
|
407
|
+
? `Re-run with --yes to confirm. Reason: ${reason}`
|
|
408
|
+
: 'Re-run with --yes to confirm, or --dry-run to preview without sending.',
|
|
409
|
+
context: { command: cmd, deviceType: typeLabel, deviceId, ...(reason ? { destructiveReason: reason } : {}) },
|
|
410
|
+
},
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
console.error(`Refusing to run destructive command "${cmd}" on ${typeLabel} without --yes.`);
|
|
415
|
+
if (reason)
|
|
416
|
+
console.error(`Reason: ${reason}`);
|
|
417
|
+
console.error(`Re-run with --yes to confirm, or --dry-run to preview without sending.`);
|
|
418
|
+
}
|
|
419
|
+
process.exit(2);
|
|
282
420
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
console.error(`Reason: ${reason}`);
|
|
287
|
-
console.error(`Re-run with --yes to confirm, or --dry-run to preview without sending.`);
|
|
421
|
+
// Warn when --yes is given but the command is not destructive (no-op flag)
|
|
422
|
+
if (options.yes && !isDestructiveCommand(cachedForGuard?.type, cmd, options.type) && !isDryRun()) {
|
|
423
|
+
console.error(`Note: --yes has no effect; "${cmd}" is not a destructive command.`);
|
|
288
424
|
}
|
|
289
|
-
process.exit(2);
|
|
290
|
-
}
|
|
291
|
-
try {
|
|
292
425
|
// parameter may be a JSON object string (e.g. S10 startClean) or a plain string
|
|
293
426
|
let parsedParam = parameter ?? 'default';
|
|
294
427
|
if (parameter) {
|
|
@@ -311,14 +444,21 @@ Examples:
|
|
|
311
444
|
printJson(result);
|
|
312
445
|
return;
|
|
313
446
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
447
|
+
if (isIr) {
|
|
448
|
+
console.log(`→ IR signal sent: ${cmd} (no feedback — fire-and-forget)`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
console.log(`✓ Command sent: ${cmd}`);
|
|
452
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
453
|
+
printKeyValue(body);
|
|
454
|
+
}
|
|
319
455
|
}
|
|
320
456
|
}
|
|
321
457
|
catch (error) {
|
|
458
|
+
// Re-throw mock process.exit signals (Vitest intercepts process.exit as thrown
|
|
459
|
+
// Error('__exit__')) so they aren't double-handled and the exit code is preserved.
|
|
460
|
+
if (error instanceof Error && error.message === '__exit__')
|
|
461
|
+
throw error;
|
|
322
462
|
handleError(error);
|
|
323
463
|
}
|
|
324
464
|
});
|
|
@@ -342,9 +482,10 @@ Examples:
|
|
|
342
482
|
printJson(catalog);
|
|
343
483
|
return;
|
|
344
484
|
}
|
|
345
|
-
const headers = ['type', 'category', 'commands', 'aliases'];
|
|
485
|
+
const headers = ['type', 'role', 'category', 'commands', 'aliases'];
|
|
346
486
|
const rows = catalog.map((e) => [
|
|
347
487
|
e.type,
|
|
488
|
+
e.role ?? '—',
|
|
348
489
|
e.category,
|
|
349
490
|
String(e.commands.length),
|
|
350
491
|
(e.aliases ?? []).join(', ') || '—',
|
|
@@ -404,7 +545,7 @@ Examples:
|
|
|
404
545
|
.command('describe')
|
|
405
546
|
.description('Describe a device by ID: metadata + supported commands + status fields (1 API call)')
|
|
406
547
|
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
407
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
548
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
408
549
|
.option('--live', 'Also fetch live status values and merge them into capabilities (costs 1 extra API call)')
|
|
409
550
|
.addHelpText('after', `
|
|
410
551
|
Makes a GET /v1.1/devices call to look up the device's type, then prints its
|
|
@@ -535,6 +676,7 @@ function renderCatalogEntry(entry) {
|
|
|
535
676
|
}
|
|
536
677
|
else {
|
|
537
678
|
console.log('\nCommands:');
|
|
679
|
+
const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
|
|
538
680
|
const rows = entry.commands.map((c) => {
|
|
539
681
|
const flags = [];
|
|
540
682
|
if (c.commandType === 'customize')
|
|
@@ -542,9 +684,13 @@ function renderCatalogEntry(entry) {
|
|
|
542
684
|
if (c.destructive)
|
|
543
685
|
flags.push('!destructive');
|
|
544
686
|
const label = flags.length > 0 ? `${c.command} [${flags.join(', ')}]` : c.command;
|
|
545
|
-
|
|
687
|
+
const base = [label, c.parameter, c.description];
|
|
688
|
+
return hasExamples ? [...base, (c.exampleParams ?? []).join(' | ') || ''] : base;
|
|
546
689
|
});
|
|
547
|
-
|
|
690
|
+
const tableHeaders = hasExamples
|
|
691
|
+
? ['command', 'parameter', 'description', 'example']
|
|
692
|
+
: ['command', 'parameter', 'description'];
|
|
693
|
+
printTable(tableHeaders, rows);
|
|
548
694
|
const hasDestructive = entry.commands.some((c) => c.destructive);
|
|
549
695
|
if (hasDestructive) {
|
|
550
696
|
console.log('\n[!destructive] commands have hard-to-reverse real-world effects — confirm before issuing.');
|