@switchbot/openapi-cli 3.2.1 → 3.3.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.
Files changed (332) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +57419 -170
  3. package/package.json +9 -5
  4. package/dist/api/client.d.ts +0 -31
  5. package/dist/api/client.js +0 -236
  6. package/dist/api/client.js.map +0 -1
  7. package/dist/auth.d.ts +0 -1
  8. package/dist/auth.js +0 -21
  9. package/dist/auth.js.map +0 -1
  10. package/dist/commands/agent-bootstrap.d.ts +0 -10
  11. package/dist/commands/agent-bootstrap.js +0 -200
  12. package/dist/commands/agent-bootstrap.js.map +0 -1
  13. package/dist/commands/auth.d.ts +0 -18
  14. package/dist/commands/auth.js +0 -355
  15. package/dist/commands/auth.js.map +0 -1
  16. package/dist/commands/batch.d.ts +0 -2
  17. package/dist/commands/batch.js +0 -414
  18. package/dist/commands/batch.js.map +0 -1
  19. package/dist/commands/cache.d.ts +0 -2
  20. package/dist/commands/cache.js +0 -127
  21. package/dist/commands/cache.js.map +0 -1
  22. package/dist/commands/capabilities.d.ts +0 -31
  23. package/dist/commands/capabilities.js +0 -383
  24. package/dist/commands/capabilities.js.map +0 -1
  25. package/dist/commands/catalog.d.ts +0 -2
  26. package/dist/commands/catalog.js +0 -360
  27. package/dist/commands/catalog.js.map +0 -1
  28. package/dist/commands/completion.d.ts +0 -2
  29. package/dist/commands/completion.js +0 -386
  30. package/dist/commands/completion.js.map +0 -1
  31. package/dist/commands/config.d.ts +0 -21
  32. package/dist/commands/config.js +0 -377
  33. package/dist/commands/config.js.map +0 -1
  34. package/dist/commands/daemon.d.ts +0 -2
  35. package/dist/commands/daemon.js +0 -411
  36. package/dist/commands/daemon.js.map +0 -1
  37. package/dist/commands/device-meta.d.ts +0 -2
  38. package/dist/commands/device-meta.js +0 -160
  39. package/dist/commands/device-meta.js.map +0 -1
  40. package/dist/commands/devices.d.ts +0 -2
  41. package/dist/commands/devices.js +0 -949
  42. package/dist/commands/devices.js.map +0 -1
  43. package/dist/commands/doctor.d.ts +0 -3
  44. package/dist/commands/doctor.js +0 -1016
  45. package/dist/commands/doctor.js.map +0 -1
  46. package/dist/commands/events.d.ts +0 -31
  47. package/dist/commands/events.js +0 -564
  48. package/dist/commands/events.js.map +0 -1
  49. package/dist/commands/expand.d.ts +0 -2
  50. package/dist/commands/expand.js +0 -131
  51. package/dist/commands/expand.js.map +0 -1
  52. package/dist/commands/explain.d.ts +0 -2
  53. package/dist/commands/explain.js +0 -140
  54. package/dist/commands/explain.js.map +0 -1
  55. package/dist/commands/health.d.ts +0 -8
  56. package/dist/commands/health.js +0 -114
  57. package/dist/commands/health.js.map +0 -1
  58. package/dist/commands/history.d.ts +0 -2
  59. package/dist/commands/history.js +0 -321
  60. package/dist/commands/history.js.map +0 -1
  61. package/dist/commands/identity.d.ts +0 -45
  62. package/dist/commands/identity.js +0 -60
  63. package/dist/commands/identity.js.map +0 -1
  64. package/dist/commands/install.d.ts +0 -20
  65. package/dist/commands/install.js +0 -247
  66. package/dist/commands/install.js.map +0 -1
  67. package/dist/commands/mcp.d.ts +0 -14
  68. package/dist/commands/mcp.js +0 -2018
  69. package/dist/commands/mcp.js.map +0 -1
  70. package/dist/commands/plan.d.ts +0 -51
  71. package/dist/commands/plan.js +0 -654
  72. package/dist/commands/plan.js.map +0 -1
  73. package/dist/commands/policy.d.ts +0 -24
  74. package/dist/commands/policy.js +0 -587
  75. package/dist/commands/policy.js.map +0 -1
  76. package/dist/commands/quota.d.ts +0 -2
  77. package/dist/commands/quota.js +0 -79
  78. package/dist/commands/quota.js.map +0 -1
  79. package/dist/commands/rules.d.ts +0 -2
  80. package/dist/commands/rules.js +0 -876
  81. package/dist/commands/rules.js.map +0 -1
  82. package/dist/commands/scenes.d.ts +0 -2
  83. package/dist/commands/scenes.js +0 -265
  84. package/dist/commands/scenes.js.map +0 -1
  85. package/dist/commands/schema.d.ts +0 -2
  86. package/dist/commands/schema.js +0 -185
  87. package/dist/commands/schema.js.map +0 -1
  88. package/dist/commands/status-sync.d.ts +0 -2
  89. package/dist/commands/status-sync.js +0 -132
  90. package/dist/commands/status-sync.js.map +0 -1
  91. package/dist/commands/uninstall.d.ts +0 -20
  92. package/dist/commands/uninstall.js +0 -238
  93. package/dist/commands/uninstall.js.map +0 -1
  94. package/dist/commands/upgrade-check.d.ts +0 -2
  95. package/dist/commands/upgrade-check.js +0 -107
  96. package/dist/commands/upgrade-check.js.map +0 -1
  97. package/dist/commands/watch.d.ts +0 -2
  98. package/dist/commands/watch.js +0 -195
  99. package/dist/commands/watch.js.map +0 -1
  100. package/dist/commands/webhook.d.ts +0 -2
  101. package/dist/commands/webhook.js +0 -183
  102. package/dist/commands/webhook.js.map +0 -1
  103. package/dist/config.d.ts +0 -57
  104. package/dist/config.js +0 -259
  105. package/dist/config.js.map +0 -1
  106. package/dist/credentials/backends/file.d.ts +0 -18
  107. package/dist/credentials/backends/file.js +0 -102
  108. package/dist/credentials/backends/file.js.map +0 -1
  109. package/dist/credentials/backends/linux.d.ts +0 -16
  110. package/dist/credentials/backends/linux.js +0 -130
  111. package/dist/credentials/backends/linux.js.map +0 -1
  112. package/dist/credentials/backends/macos.d.ts +0 -18
  113. package/dist/credentials/backends/macos.js +0 -130
  114. package/dist/credentials/backends/macos.js.map +0 -1
  115. package/dist/credentials/backends/windows.d.ts +0 -23
  116. package/dist/credentials/backends/windows.js +0 -216
  117. package/dist/credentials/backends/windows.js.map +0 -1
  118. package/dist/credentials/keychain.d.ts +0 -83
  119. package/dist/credentials/keychain.js +0 -89
  120. package/dist/credentials/keychain.js.map +0 -1
  121. package/dist/credentials/prime.d.ts +0 -32
  122. package/dist/credentials/prime.js +0 -53
  123. package/dist/credentials/prime.js.map +0 -1
  124. package/dist/devices/cache.d.ts +0 -79
  125. package/dist/devices/cache.js +0 -294
  126. package/dist/devices/cache.js.map +0 -1
  127. package/dist/devices/catalog.d.ts +0 -138
  128. package/dist/devices/catalog.js +0 -768
  129. package/dist/devices/catalog.js.map +0 -1
  130. package/dist/devices/device-meta.d.ts +0 -15
  131. package/dist/devices/device-meta.js +0 -57
  132. package/dist/devices/device-meta.js.map +0 -1
  133. package/dist/devices/history-agg.d.ts +0 -37
  134. package/dist/devices/history-agg.js +0 -139
  135. package/dist/devices/history-agg.js.map +0 -1
  136. package/dist/devices/history-query.d.ts +0 -45
  137. package/dist/devices/history-query.js +0 -182
  138. package/dist/devices/history-query.js.map +0 -1
  139. package/dist/devices/param-validator.d.ts +0 -40
  140. package/dist/devices/param-validator.js +0 -434
  141. package/dist/devices/param-validator.js.map +0 -1
  142. package/dist/devices/resources.d.ts +0 -74
  143. package/dist/devices/resources.js +0 -271
  144. package/dist/devices/resources.js.map +0 -1
  145. package/dist/index.d.ts +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/install/default-steps.d.ts +0 -66
  148. package/dist/install/default-steps.js +0 -258
  149. package/dist/install/default-steps.js.map +0 -1
  150. package/dist/install/preflight.d.ts +0 -60
  151. package/dist/install/preflight.js +0 -213
  152. package/dist/install/preflight.js.map +0 -1
  153. package/dist/install/steps.d.ts +0 -61
  154. package/dist/install/steps.js +0 -68
  155. package/dist/install/steps.js.map +0 -1
  156. package/dist/lib/command-keywords.d.ts +0 -5
  157. package/dist/lib/command-keywords.js +0 -18
  158. package/dist/lib/command-keywords.js.map +0 -1
  159. package/dist/lib/daemon-state.d.ts +0 -24
  160. package/dist/lib/daemon-state.js +0 -47
  161. package/dist/lib/daemon-state.js.map +0 -1
  162. package/dist/lib/destructive-mode.d.ts +0 -2
  163. package/dist/lib/destructive-mode.js +0 -13
  164. package/dist/lib/destructive-mode.js.map +0 -1
  165. package/dist/lib/devices.d.ts +0 -151
  166. package/dist/lib/devices.js +0 -383
  167. package/dist/lib/devices.js.map +0 -1
  168. package/dist/lib/idempotency.d.ts +0 -46
  169. package/dist/lib/idempotency.js +0 -107
  170. package/dist/lib/idempotency.js.map +0 -1
  171. package/dist/lib/plan-store.d.ts +0 -19
  172. package/dist/lib/plan-store.js +0 -69
  173. package/dist/lib/plan-store.js.map +0 -1
  174. package/dist/lib/request-context.d.ts +0 -7
  175. package/dist/lib/request-context.js +0 -13
  176. package/dist/lib/request-context.js.map +0 -1
  177. package/dist/lib/scenes.d.ts +0 -7
  178. package/dist/lib/scenes.js +0 -11
  179. package/dist/lib/scenes.js.map +0 -1
  180. package/dist/logger.d.ts +0 -4
  181. package/dist/logger.js +0 -17
  182. package/dist/logger.js.map +0 -1
  183. package/dist/mcp/device-history.d.ts +0 -36
  184. package/dist/mcp/device-history.js +0 -146
  185. package/dist/mcp/device-history.js.map +0 -1
  186. package/dist/mcp/events-subscription.d.ts +0 -45
  187. package/dist/mcp/events-subscription.js +0 -214
  188. package/dist/mcp/events-subscription.js.map +0 -1
  189. package/dist/mqtt/client.d.ts +0 -25
  190. package/dist/mqtt/client.js +0 -181
  191. package/dist/mqtt/client.js.map +0 -1
  192. package/dist/mqtt/credential.d.ts +0 -16
  193. package/dist/mqtt/credential.js +0 -31
  194. package/dist/mqtt/credential.js.map +0 -1
  195. package/dist/policy/add-rule.d.ts +0 -21
  196. package/dist/policy/add-rule.js +0 -125
  197. package/dist/policy/add-rule.js.map +0 -1
  198. package/dist/policy/diff.d.ts +0 -21
  199. package/dist/policy/diff.js +0 -92
  200. package/dist/policy/diff.js.map +0 -1
  201. package/dist/policy/format.d.ts +0 -6
  202. package/dist/policy/format.js +0 -58
  203. package/dist/policy/format.js.map +0 -1
  204. package/dist/policy/load.d.ts +0 -32
  205. package/dist/policy/load.js +0 -62
  206. package/dist/policy/load.js.map +0 -1
  207. package/dist/policy/migrate.d.ts +0 -21
  208. package/dist/policy/migrate.js +0 -68
  209. package/dist/policy/migrate.js.map +0 -1
  210. package/dist/policy/schema.d.ts +0 -5
  211. package/dist/policy/schema.js +0 -19
  212. package/dist/policy/schema.js.map +0 -1
  213. package/dist/policy/validate.d.ts +0 -19
  214. package/dist/policy/validate.js +0 -263
  215. package/dist/policy/validate.js.map +0 -1
  216. package/dist/rules/action.d.ts +0 -65
  217. package/dist/rules/action.js +0 -217
  218. package/dist/rules/action.js.map +0 -1
  219. package/dist/rules/audit-query.d.ts +0 -51
  220. package/dist/rules/audit-query.js +0 -90
  221. package/dist/rules/audit-query.js.map +0 -1
  222. package/dist/rules/conflict-analyzer.d.ts +0 -57
  223. package/dist/rules/conflict-analyzer.js +0 -215
  224. package/dist/rules/conflict-analyzer.js.map +0 -1
  225. package/dist/rules/cron-scheduler.d.ts +0 -62
  226. package/dist/rules/cron-scheduler.js +0 -187
  227. package/dist/rules/cron-scheduler.js.map +0 -1
  228. package/dist/rules/destructive.d.ts +0 -20
  229. package/dist/rules/destructive.js +0 -53
  230. package/dist/rules/destructive.js.map +0 -1
  231. package/dist/rules/engine.d.ts +0 -193
  232. package/dist/rules/engine.js +0 -758
  233. package/dist/rules/engine.js.map +0 -1
  234. package/dist/rules/matcher.d.ts +0 -56
  235. package/dist/rules/matcher.js +0 -231
  236. package/dist/rules/matcher.js.map +0 -1
  237. package/dist/rules/pid-file.d.ts +0 -43
  238. package/dist/rules/pid-file.js +0 -96
  239. package/dist/rules/pid-file.js.map +0 -1
  240. package/dist/rules/quiet-hours.d.ts +0 -26
  241. package/dist/rules/quiet-hours.js +0 -46
  242. package/dist/rules/quiet-hours.js.map +0 -1
  243. package/dist/rules/suggest.d.ts +0 -20
  244. package/dist/rules/suggest.js +0 -96
  245. package/dist/rules/suggest.js.map +0 -1
  246. package/dist/rules/throttle.d.ts +0 -61
  247. package/dist/rules/throttle.js +0 -117
  248. package/dist/rules/throttle.js.map +0 -1
  249. package/dist/rules/types.d.ts +0 -117
  250. package/dist/rules/types.js +0 -35
  251. package/dist/rules/types.js.map +0 -1
  252. package/dist/rules/webhook-listener.d.ts +0 -63
  253. package/dist/rules/webhook-listener.js +0 -224
  254. package/dist/rules/webhook-listener.js.map +0 -1
  255. package/dist/rules/webhook-token.d.ts +0 -50
  256. package/dist/rules/webhook-token.js +0 -91
  257. package/dist/rules/webhook-token.js.map +0 -1
  258. package/dist/schema/field-aliases.d.ts +0 -34
  259. package/dist/schema/field-aliases.js +0 -132
  260. package/dist/schema/field-aliases.js.map +0 -1
  261. package/dist/sinks/dispatcher.d.ts +0 -7
  262. package/dist/sinks/dispatcher.js +0 -13
  263. package/dist/sinks/dispatcher.js.map +0 -1
  264. package/dist/sinks/file.d.ts +0 -6
  265. package/dist/sinks/file.js +0 -20
  266. package/dist/sinks/file.js.map +0 -1
  267. package/dist/sinks/format.d.ts +0 -20
  268. package/dist/sinks/format.js +0 -57
  269. package/dist/sinks/format.js.map +0 -1
  270. package/dist/sinks/homeassistant.d.ts +0 -18
  271. package/dist/sinks/homeassistant.js +0 -45
  272. package/dist/sinks/homeassistant.js.map +0 -1
  273. package/dist/sinks/openclaw.d.ts +0 -13
  274. package/dist/sinks/openclaw.js +0 -34
  275. package/dist/sinks/openclaw.js.map +0 -1
  276. package/dist/sinks/stdout.d.ts +0 -4
  277. package/dist/sinks/stdout.js +0 -6
  278. package/dist/sinks/stdout.js.map +0 -1
  279. package/dist/sinks/telegram.d.ts +0 -11
  280. package/dist/sinks/telegram.js +0 -29
  281. package/dist/sinks/telegram.js.map +0 -1
  282. package/dist/sinks/types.d.ts +0 -13
  283. package/dist/sinks/types.js +0 -2
  284. package/dist/sinks/types.js.map +0 -1
  285. package/dist/sinks/webhook.d.ts +0 -6
  286. package/dist/sinks/webhook.js +0 -23
  287. package/dist/sinks/webhook.js.map +0 -1
  288. package/dist/status-sync/manager.d.ts +0 -48
  289. package/dist/status-sync/manager.js +0 -269
  290. package/dist/status-sync/manager.js.map +0 -1
  291. package/dist/utils/arg-parsers.d.ts +0 -16
  292. package/dist/utils/arg-parsers.js +0 -67
  293. package/dist/utils/arg-parsers.js.map +0 -1
  294. package/dist/utils/audit.d.ts +0 -69
  295. package/dist/utils/audit.js +0 -122
  296. package/dist/utils/audit.js.map +0 -1
  297. package/dist/utils/filter.d.ts +0 -81
  298. package/dist/utils/filter.js +0 -190
  299. package/dist/utils/filter.js.map +0 -1
  300. package/dist/utils/flags.d.ts +0 -72
  301. package/dist/utils/flags.js +0 -187
  302. package/dist/utils/flags.js.map +0 -1
  303. package/dist/utils/format.d.ts +0 -9
  304. package/dist/utils/format.js +0 -118
  305. package/dist/utils/format.js.map +0 -1
  306. package/dist/utils/health.d.ts +0 -48
  307. package/dist/utils/health.js +0 -102
  308. package/dist/utils/health.js.map +0 -1
  309. package/dist/utils/help-json.d.ts +0 -39
  310. package/dist/utils/help-json.js +0 -55
  311. package/dist/utils/help-json.js.map +0 -1
  312. package/dist/utils/name-resolver.d.ts +0 -26
  313. package/dist/utils/name-resolver.js +0 -138
  314. package/dist/utils/name-resolver.js.map +0 -1
  315. package/dist/utils/output.d.ts +0 -73
  316. package/dist/utils/output.js +0 -405
  317. package/dist/utils/output.js.map +0 -1
  318. package/dist/utils/quota.d.ts +0 -61
  319. package/dist/utils/quota.js +0 -228
  320. package/dist/utils/quota.js.map +0 -1
  321. package/dist/utils/redact.d.ts +0 -23
  322. package/dist/utils/redact.js +0 -69
  323. package/dist/utils/redact.js.map +0 -1
  324. package/dist/utils/retry.d.ts +0 -72
  325. package/dist/utils/retry.js +0 -141
  326. package/dist/utils/retry.js.map +0 -1
  327. package/dist/utils/string.d.ts +0 -2
  328. package/dist/utils/string.js +0 -23
  329. package/dist/utils/string.js.map +0 -1
  330. package/dist/version.d.ts +0 -2
  331. package/dist/version.js +0 -5
  332. package/dist/version.js.map +0 -1
@@ -1,949 +0,0 @@
1
- import { enumArg, stringArg } from '../utils/arg-parsers.js';
2
- import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError, StructuredUsageError, exitWithError } from '../utils/output.js';
3
- import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
4
- import { findCatalogEntry, getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
5
- import { getCachedDevice, loadCache } from '../devices/cache.js';
6
- import { loadDeviceMeta } from '../devices/device-meta.js';
7
- import { resolveDeviceId, ALL_STRATEGIES } from '../utils/name-resolver.js';
8
- import { fetchDeviceList, fetchDeviceStatus, executeCommand, describeDevice, validateCommand, isDestructiveCommand, getDestructiveReason, buildHubLocationMap, DeviceNotFoundError, } from '../lib/devices.js';
9
- import { parseFilterExpr, matchClause, FilterSyntaxError } from '../utils/filter.js';
10
- import { validateParameter } from '../devices/param-validator.js';
11
- import { registerBatchCommand } from './batch.js';
12
- import { registerWatchCommand } from './watch.js';
13
- import { registerExplainCommand } from './explain.js';
14
- import { registerExpandCommand } from './expand.js';
15
- import { registerDevicesMetaCommand } from './device-meta.js';
16
- import { isDryRun } from '../utils/flags.js';
17
- import { DryRunSignal } from '../api/client.js';
18
- import { resolveField, resolveFieldList, listSupportedFieldInputs } from '../schema/field-aliases.js';
19
- import { allowsDirectDestructiveExecution, destructiveExecutionHint } from '../lib/destructive-mode.js';
20
- const EXPAND_HINTS = {
21
- 'Air Conditioner': { command: 'setAll', flags: '--temp 26 --mode cool --fan low --power on' },
22
- 'Curtain': { command: 'setPosition', flags: '--position 50 --mode silent' },
23
- 'Curtain 3': { command: 'setPosition', flags: '--position 50' },
24
- 'Blind Tilt': { command: 'setPosition', flags: '--direction up --angle 50' },
25
- 'Relay Switch 2PM': { command: 'setMode', flags: '--channel 1 --mode edge' },
26
- };
27
- export function registerDevicesCommand(program) {
28
- const COMMAND_TYPES = ['command', 'customize'];
29
- const devices = program
30
- .command('devices')
31
- .description('Manage and control SwitchBot devices')
32
- .addHelpText('after', `
33
- Typical workflow:
34
- 1. Discover your devices → switchbot devices list
35
- 2. Describe a specific device → switchbot devices describe <id>
36
- 3. Or look up a type offline → switchbot devices types
37
- switchbot devices commands <type>
38
- 4. Send a command → switchbot devices command <id> <cmd> [param]
39
-
40
- Online subcommands (hit the SwitchBot API):
41
- list List all physical + IR remote devices on your account
42
- status Query a device's real-time status values
43
- command Send a control command (turnOn, setColor, setAll, startClean, …)
44
- describe Show one device's metadata + its supported commands + status fields
45
-
46
- Offline subcommands (built-in catalog, no API call):
47
- types List every device type this CLI knows about
48
- commands Show commands + parameter formats + status fields for a type
49
-
50
- Run any subcommand with --help for its own flags and examples.
51
- `);
52
- // switchbot devices list
53
- devices
54
- .command('list')
55
- .alias('ls')
56
- .description('List all physical devices and IR remote devices on the account')
57
- .addHelpText('after', `
58
- Default columns: deviceId, deviceName, type, category
59
- Pass --wide for the full operator view: + controlType, family, roomID, room, hub, cloud
60
- --fields accepts any subset of all column names (exit 2 on unknown names).
61
-
62
- type - physical deviceType (e.g. "Bot", "Curtain") or IR remoteType (e.g. "TV")
63
- category - "physical" or "ir"
64
- controlType - functional classification from the API (e.g. "Bot", "Switch",
65
- "TV") — may differ from 'type' and groups devices by behavior
66
- family - home/family name (IR remotes inherit this from their bound Hub)
67
- roomID - internal room identifier (IR remotes inherit from their
68
- bound Hub; — when unassigned/unknown)
69
- room - room name this device is assigned to (IR remotes inherit from
70
- Hub; — when unassigned/unknown)
71
- hub - "—" when the device is its own hub or hubDeviceId is empty
72
- cloud - ✓/✗: whether cloud service is enabled (— for IR remotes)
73
-
74
- controlType, family/room, and roomID require the 'src: OpenClaw' header, which
75
- this CLI always sends. (IR family/room inheritance is computed client-side for
76
- the table; --json returns the raw API body unchanged.)
77
-
78
- Examples:
79
- $ switchbot devices list
80
- $ switchbot devices list --wide
81
- $ switchbot devices list --format tsv --fields deviceId,deviceName,type,category
82
- $ switchbot devices list --json | jq '.deviceList[] | select(.familyName == "home")'
83
- $ switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
84
- $ switchbot devices list --filter type="Air Conditioner"
85
- $ switchbot devices list --filter category=ir
86
- $ switchbot devices list --filter name=living,category=physical
87
- $ switchbot devices list --filter 'name~living' # explicit substring
88
- $ switchbot devices list --filter 'type=/Air.*/' # regex (case-insensitive)
89
- `)
90
- .option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
91
- .option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
92
- .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'))
93
- .action(async (options) => {
94
- try {
95
- const body = await fetchDeviceList();
96
- const { deviceList, infraredRemoteList } = body;
97
- const fmt = resolveFormat();
98
- const deviceMeta = loadDeviceMeta();
99
- const hubLocation = buildHubLocationMap(deviceList);
100
- // Parse --filter into a list of clauses. Shared grammar across
101
- // `devices list`, `devices batch`, and `events tail` / `mqtt-tail`.
102
- const LIST_KEYS = ['deviceId', 'type', 'name', 'category', 'room', 'controlType',
103
- 'family', 'hub', 'roomID', 'cloud', 'alias'];
104
- const LIST_FILTER_CANONICAL = ['deviceId', 'deviceName', 'deviceType', 'controlType',
105
- 'roomName', 'category', 'familyName', 'hubDeviceId', 'roomID',
106
- 'enableCloudService', 'alias'];
107
- const LIST_FILTER_TO_RUNTIME = {
108
- deviceId: 'deviceId',
109
- deviceName: 'name',
110
- deviceType: 'type',
111
- controlType: 'controlType',
112
- roomName: 'room',
113
- category: 'category',
114
- familyName: 'family',
115
- hubDeviceId: 'hub',
116
- roomID: 'roomID',
117
- enableCloudService: 'cloud',
118
- alias: 'alias',
119
- };
120
- let listClauses = null;
121
- if (options.filter) {
122
- try {
123
- listClauses = parseFilterExpr(options.filter, LIST_KEYS, {
124
- resolveKey: (input) => {
125
- const canonical = resolveField(input, LIST_FILTER_CANONICAL);
126
- return LIST_FILTER_TO_RUNTIME[canonical];
127
- },
128
- supportedKeys: listSupportedFieldInputs(LIST_FILTER_CANONICAL),
129
- });
130
- }
131
- catch (err) {
132
- if (err instanceof FilterSyntaxError)
133
- throw new UsageError(err.message);
134
- throw err;
135
- }
136
- }
137
- const matchesFilter = (entry) => {
138
- if (!listClauses || listClauses.length === 0)
139
- return true;
140
- for (const c of listClauses) {
141
- const fieldVal = entry[c.key] ?? '';
142
- if (!matchClause(fieldVal, c))
143
- return false;
144
- }
145
- return true;
146
- };
147
- if (fmt === 'json' && process.argv.includes('--json')) {
148
- if (listClauses) {
149
- 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 || '' }));
150
- const filteredIrList = infraredRemoteList.filter((d) => {
151
- const inherited = hubLocation.get(d.hubDeviceId);
152
- 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 || '' });
153
- });
154
- printJson({ ok: true, deviceList: filteredDeviceList, infraredRemoteList: filteredIrList });
155
- }
156
- else {
157
- printJson({ ok: true, ...body });
158
- }
159
- return;
160
- }
161
- const narrowHeaders = ['deviceId', 'deviceName', 'type', 'category'];
162
- const wideHeaders = ['deviceId', 'deviceName', 'type', 'category', 'controlType', 'family', 'roomID', 'room', 'hub', 'cloud', 'alias'];
163
- const userFields = resolveFields();
164
- const headers = userFields ? wideHeaders : (options.wide ? wideHeaders : narrowHeaders);
165
- const rows = [];
166
- for (const d of deviceList) {
167
- if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
168
- continue;
169
- 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 || '' }))
170
- continue;
171
- rows.push([
172
- d.deviceId,
173
- d.deviceName,
174
- d.deviceType || '—',
175
- 'physical',
176
- d.controlType || '—',
177
- d.familyName || '—',
178
- d.roomID || '—',
179
- d.roomName || '—',
180
- !d.hubDeviceId || d.hubDeviceId === '000000000000' ? '—' : d.hubDeviceId,
181
- d.enableCloudService,
182
- deviceMeta.devices[d.deviceId]?.alias ?? '—',
183
- ]);
184
- }
185
- for (const d of infraredRemoteList) {
186
- if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
187
- continue;
188
- const inherited = hubLocation.get(d.hubDeviceId);
189
- 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 || '' }))
190
- continue;
191
- rows.push([
192
- d.deviceId,
193
- d.deviceName,
194
- d.remoteType,
195
- 'ir',
196
- d.controlType || '—',
197
- inherited?.family || '—',
198
- inherited?.roomID || '—',
199
- inherited?.room || '—',
200
- d.hubDeviceId,
201
- null,
202
- deviceMeta.devices[d.deviceId]?.alias ?? '—',
203
- ]);
204
- }
205
- if (rows.length === 0 && fmt === 'table') {
206
- console.log(listClauses ? 'No devices matched the filter.' : 'No devices found');
207
- return;
208
- }
209
- const defaultFields = options.wide ? undefined : narrowHeaders;
210
- // Accept API field names and short aliases alongside canonical column names
211
- const DEVICE_LIST_ALIASES = {
212
- id: 'deviceId',
213
- name: 'deviceName',
214
- deviceType: 'type',
215
- type: 'type',
216
- roomName: 'room',
217
- familyName: 'family',
218
- hubDeviceId: 'hub',
219
- enableCloudService: 'cloud',
220
- controlType: 'controlType',
221
- deviceName: 'deviceName',
222
- deviceId: 'deviceId',
223
- category: 'category',
224
- alias: 'alias',
225
- };
226
- renderRows(wideHeaders, rows, fmt, userFields ?? defaultFields, DEVICE_LIST_ALIASES);
227
- if (fmt === 'table') {
228
- const totalLabel = listClauses
229
- ? `${rows.length} match(es) (${deviceList.length} physical + ${infraredRemoteList.length} IR before filter)`
230
- : `${deviceList.length} physical device(s), ${infraredRemoteList.length} IR remote device(s)`;
231
- console.log(`\nTotal: ${totalLabel}`);
232
- console.log(`Tip: 'switchbot devices describe <deviceId>' shows a device's supported commands.`);
233
- }
234
- }
235
- catch (error) {
236
- handleError(error);
237
- }
238
- });
239
- // switchbot devices status <deviceId>
240
- devices
241
- .command('status')
242
- .description('Query the real-time status of a specific device')
243
- .argument('[deviceId]', 'Device ID from "devices list" (or use --name or --ids)')
244
- .option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
245
- .option('--name-strategy <s>', `Name match strategy: ${ALL_STRATEGIES.join('|')} (default: fuzzy)`, stringArg('--name-strategy'))
246
- .option('--name-type <type>', 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg('--name-type'))
247
- .option('--name-category <cat>', 'Narrow --name by category: physical|ir', enumArg('--name-category', ['physical', 'ir']))
248
- .option('--name-room <room>', 'Narrow --name by room name (substring match)', stringArg('--name-room'))
249
- .option('--ids <list>', 'Comma-separated device IDs for batch status (incompatible with --name)', stringArg('--ids'))
250
- .addHelpText('after', `
251
- Status fields vary by device type. To discover them without a live call:
252
-
253
- switchbot devices commands <type> (prints the "Status fields" section)
254
-
255
- For --fields: run the command once with --format yaml (no --fields) to see
256
- all field names returned by your specific device, then narrow with --fields.
257
-
258
- Examples:
259
- $ switchbot devices status ABC123DEF456
260
- $ switchbot devices status --name "Living Room AC"
261
- $ switchbot devices status ABC123DEF456 --json
262
- $ switchbot devices status ABC123DEF456 --format yaml
263
- $ switchbot devices status ABC123DEF456 --format tsv --fields power,battery
264
- $ switchbot devices status ABC123DEF456 --json | jq '.data.battery'
265
- $ switchbot devices status --ids ABC123,DEF456,GHI789
266
- $ switchbot devices status --ids ABC123,DEF456 --fields power,battery
267
- `)
268
- .action(async (deviceIdArg, options) => {
269
- try {
270
- // Batch mode: --ids id1,id2,id3
271
- if (options.ids) {
272
- if (options.name)
273
- throw new UsageError('--ids and --name cannot be used together.');
274
- const ids = options.ids.split(',').map((s) => s.trim()).filter(Boolean);
275
- if (ids.length === 0)
276
- throw new UsageError('--ids requires at least one device ID.');
277
- const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
278
- const fetchedAt = new Date().toISOString();
279
- const batch = results.map((r, i) => r.status === 'fulfilled'
280
- ? { deviceId: ids[i], ok: true, _fetchedAt: fetchedAt, ...r.value }
281
- : { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) });
282
- const batchFmt = resolveFormat();
283
- if (isJsonMode() || batchFmt === 'json') {
284
- printJson(batch);
285
- }
286
- else if (batchFmt === 'jsonl') {
287
- for (const entry of batch) {
288
- console.log(JSON.stringify(entry));
289
- }
290
- }
291
- else {
292
- const rawFields = resolveFields();
293
- for (const entry of batch) {
294
- const { deviceId, ok, error, _fetchedAt: ts, ...status } = entry;
295
- console.log(`\n─── ${String(deviceId)} ───`);
296
- if (!ok) {
297
- console.error(` error: ${String(error)}`);
298
- }
299
- else {
300
- const statusMap = status;
301
- const fields = rawFields
302
- ? resolveFieldList(rawFields, Object.keys(statusMap))
303
- : undefined;
304
- const displayStatus = fields
305
- ? Object.fromEntries(fields.map((f) => [f, statusMap[f] ?? null]))
306
- : statusMap;
307
- printKeyValue(displayStatus);
308
- console.error(` fetched at ${String(ts)}`);
309
- }
310
- }
311
- }
312
- return;
313
- }
314
- const deviceId = resolveDeviceId(deviceIdArg, options.name, {
315
- strategy: options.nameStrategy ?? 'fuzzy',
316
- type: options.nameType,
317
- category: options.nameCategory,
318
- room: options.nameRoom,
319
- });
320
- const body = await fetchDeviceStatus(deviceId);
321
- const fetchedAt = new Date().toISOString();
322
- const fmt = resolveFormat();
323
- if (fmt === 'json' && process.argv.includes('--json')) {
324
- printJson({ ...body, _fetchedAt: fetchedAt });
325
- return;
326
- }
327
- if (fmt !== 'table') {
328
- const statusWithTs = { ...body, _fetchedAt: fetchedAt };
329
- const allHeaders = Object.keys(statusWithTs);
330
- const allRows = [Object.values(statusWithTs)];
331
- const rawFields = resolveFields();
332
- const fields = rawFields
333
- ? resolveFieldList(rawFields, allHeaders)
334
- : undefined;
335
- renderRows(allHeaders, allRows, fmt, fields);
336
- return;
337
- }
338
- printKeyValue(body);
339
- console.error(`\nfetched at ${fetchedAt}`);
340
- }
341
- catch (error) {
342
- handleError(error);
343
- }
344
- });
345
- // switchbot devices command <deviceId> <command> [parameter]
346
- devices
347
- .command('command')
348
- .description('Send a control command to a device')
349
- .argument('[deviceId]', 'Target device ID (or use --name)')
350
- .argument('[cmd]', 'Command name, e.g. turnOn, turnOff, setColor, setBrightness, setAll, startClean')
351
- .argument('[parameter]', 'Command parameter. Omit for commands like turnOn/turnOff (defaults to "default"). Format depends on the command (see below). Negative numbers like -1 are accepted as-is (use `--` before them only if Commander mis-parses in your shell).')
352
- .allowUnknownOption()
353
- .option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
354
- .option('--name-strategy <s>', `Name match strategy: ${ALL_STRATEGIES.join('|')} (default for command: require-unique)`, stringArg('--name-strategy'))
355
- .option('--name-type <type>', 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg('--name-type'))
356
- .option('--name-category <cat>', 'Narrow --name by category: physical|ir', enumArg('--name-category', ['physical', 'ir']))
357
- .option('--name-room <room>', 'Narrow --name by room name (substring match)', stringArg('--name-room'))
358
- .option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
359
- .option('--yes', 'Confirm a destructive command in an explicit dev profile. --dry-run is always allowed without --yes.')
360
- .option('--explain', 'Print a human-readable summary of what this command would do (risk level, device type, idempotency) then exit without executing.')
361
- .option('--allow-unknown-device', 'Allow targeting a deviceId that is not in the local cache. By default unknown IDs exit 2 so --dry-run is a reliable pre-flight gate; use this flag for scripted pass-through.')
362
- .option('--skip-param-validation', 'Skip client-side parameter validation (escape hatch — prefer fixing the argument over using this).')
363
- .option('--idempotency-key <key>', 'Client-supplied key to dedupe retries. process-local 60s window; cache is per Node process (MCP session, batch run, plan run). Independent CLI invocations do not share cache.', stringArg('--idempotency-key'))
364
- .addHelpText('after', `
365
- ────────────────────────────────────────────────────────────────────────
366
- For the full list of commands a specific device supports — and their
367
- exact parameter formats — run:
368
-
369
- switchbot devices commands <type> (e.g. Bot, Curtain, "Smart Lock")
370
-
371
- The catalog is the authoritative per-device reference. This page only
372
- covers the generic mechanics that apply to every device.
373
- ────────────────────────────────────────────────────────────────────────
374
-
375
- Rules:
376
- • Command names are CASE-SENSITIVE (e.g. SetChannel, FastForward, volumeAdd).
377
- • Quote any parameter containing ':' ',' ';' or '{ }' to protect it from the shell.
378
- • The parameter is parsed as JSON when possible; otherwise passed through as a string.
379
- • Omit the parameter for no-arg commands — it auto-defaults to "default".
380
- • Use --type customize to trigger a user-defined IR button by name.
381
-
382
- Generic parameter shapes (see 'devices commands <type>' for which one applies):
383
-
384
- (none) turnOn, turnOff, toggle, press, play, pause, …
385
- <integer> setBrightness 75, setColorTemperature 4000, SetChannel 15
386
- <R:G:B> setColor "255:0:0"
387
- <direction;angle> setPosition "up;60" (Blind Tilt)
388
- <a,b,c,…> setAll "26,1,3,on" (IR AC)
389
- <json object> startClean '{"action":"sweep","param":{"fanLevel":2,"times":1}}'
390
-
391
- Common errors:
392
- 160 command not supported by this device
393
- 161 device offline (BLE devices need a Hub bridge)
394
- 171 hub offline
395
-
396
- Safety:
397
- Destructive commands (Smart Lock unlock, Garage Door Opener turnOn/turnOff,
398
- Keypad createKey/deleteKey, …) are blocked by default. Use the reviewed plan
399
- flow instead, or --dry-run to preview without sending.
400
-
401
- Examples:
402
- $ switchbot devices command ABC123 turnOn
403
- $ switchbot devices command ABC123 setColor "255:0:0"
404
- $ switchbot devices command ABC123 setAll "26,1,3,on"
405
- $ switchbot devices command ABC123 startClean '{"action":"sweep","param":{"fanLevel":2,"times":1}}'
406
- $ switchbot devices command ABC123 "MyButton" --type customize
407
- $ switchbot devices command <lockId> unlock --dry-run
408
- `)
409
- .action(async (deviceIdArg, cmdArg, parameter, options) => {
410
- // Declared outside try so the DryRunSignal catch branch can reference them.
411
- let _deviceId;
412
- let _cmd;
413
- let _parsedParam;
414
- try {
415
- // BUG-FIX: When --name is provided, Commander fills positionals left-to-right
416
- // starting at [deviceId]. Shift them back to their semantic slots.
417
- let cmd;
418
- let effectiveDeviceIdArg;
419
- if (options.name) {
420
- // `--name "x" <cmd> [parameter]` → Commander binds deviceIdArg=<cmd>, cmdArg=[parameter].
421
- if (!deviceIdArg) {
422
- throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
423
- }
424
- cmd = deviceIdArg;
425
- if (cmdArg !== undefined) {
426
- if (parameter !== undefined) {
427
- throw new UsageError('Too many positional arguments after --name. Expected: --name <query> <cmd> [parameter].');
428
- }
429
- parameter = cmdArg;
430
- }
431
- effectiveDeviceIdArg = undefined;
432
- }
433
- else {
434
- if (!cmdArg) {
435
- throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
436
- }
437
- cmd = cmdArg;
438
- effectiveDeviceIdArg = deviceIdArg;
439
- }
440
- const deviceId = resolveDeviceId(effectiveDeviceIdArg, options.name, {
441
- // Mutating command → default require-unique (never silently pick between ambiguous matches).
442
- strategy: options.nameStrategy ?? 'require-unique',
443
- type: options.nameType,
444
- category: options.nameCategory,
445
- room: options.nameRoom,
446
- });
447
- _deviceId = deviceId;
448
- if (!getCachedDevice(deviceId)) {
449
- if (options.allowUnknownDevice) {
450
- console.error(`Note: device ${deviceId} is not in the local cache — run 'switchbot devices list' first to enable command validation. (--allow-unknown-device is set, continuing.)`);
451
- }
452
- else {
453
- const cache = loadCache();
454
- const allIds = cache ? Object.keys(cache.devices) : [];
455
- const candidates = allIds
456
- .filter((id) => id.toLowerCase().includes(deviceId.toLowerCase()) || id.startsWith(deviceId.slice(0, 4)))
457
- .slice(0, 5)
458
- .map((id) => {
459
- const dev = cache.devices[id];
460
- return { deviceId: id, name: dev.name, type: dev.type };
461
- });
462
- throw new StructuredUsageError(`Unknown deviceId "${deviceId}" — not in local cache. Run 'switchbot devices list' first, or pass --allow-unknown-device to bypass this check.`, {
463
- error: 'unknown_device_id',
464
- deviceId,
465
- candidates,
466
- hint: `Pass --allow-unknown-device to skip this check (and rely on the API for validation).`,
467
- });
468
- }
469
- }
470
- const validation = validateCommand(deviceId, cmd, parameter, options.type);
471
- if (!validation.ok) {
472
- const err = validation.error;
473
- let hint = err.hint;
474
- if (err.kind === 'unknown-command') {
475
- const cached = getCachedDevice(deviceId);
476
- if (cached) {
477
- const extra = `Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.\n` +
478
- `(If the catalog is out of date, run 'switchbot devices list' to refresh the local cache, or pass --type customize for custom IR buttons.)`;
479
- hint = hint ? `${hint}\n${extra}` : extra;
480
- }
481
- }
482
- exitWithError({
483
- code: 2,
484
- kind: 'usage',
485
- message: err.message,
486
- hint,
487
- context: { validationKind: err.kind },
488
- });
489
- }
490
- // Case-only mismatch: emit a warning and continue with the canonical name.
491
- if (validation.caseNormalizedFrom && validation.normalized) {
492
- console.error(`Note: '${validation.caseNormalizedFrom}' normalized to '${validation.normalized}' (case mismatch). Use exact casing to silence this warning.`);
493
- cmd = validation.normalized;
494
- }
495
- else if (validation.normalized) {
496
- cmd = validation.normalized;
497
- }
498
- // Raw-parameter validation (runs for known (deviceType, command) pairs only).
499
- const cachedForParam = getCachedDevice(deviceId);
500
- if (cachedForParam && options.type === 'command' && !options.skipParamValidation) {
501
- const paramCheck = validateParameter(cachedForParam.type, cmd, parameter);
502
- if (!paramCheck.ok) {
503
- exitWithError({
504
- message: `Error: ${paramCheck.error}`,
505
- context: { command: cmd, deviceType: cachedForParam.type, deviceId, humanHint: paramCheck.error },
506
- });
507
- }
508
- if (paramCheck.normalized !== undefined)
509
- parameter = paramCheck.normalized;
510
- }
511
- const cachedForGuard = getCachedDevice(deviceId);
512
- // --explain: print intent + risk metadata without executing
513
- if (options.explain) {
514
- const isDestructive = isDestructiveCommand(cachedForGuard?.type, cmd, options.type);
515
- const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
516
- const riskLevel = isDestructive ? 'high' : options.type === 'command' ? 'medium' : 'low';
517
- const recommendedMode = isDestructive ? 'review-before-execute' : 'direct';
518
- if (isJsonMode()) {
519
- printJson({
520
- intent: `Send command "${cmd}" to device ${deviceId}`,
521
- deviceType: cachedForGuard?.type ?? 'unknown',
522
- deviceName: cachedForGuard?.name ?? null,
523
- command: cmd,
524
- parameter: parameter ?? null,
525
- commandType: options.type,
526
- riskLevel,
527
- requiresConfirmation: isDestructive,
528
- safetyReason: reason ?? null,
529
- recommendedMode,
530
- note: 'This is a dry explanation only — command was NOT executed.',
531
- });
532
- }
533
- else {
534
- console.log(`Command: ${cmd} on device ${deviceId}`);
535
- console.log(`Device type: ${cachedForGuard?.type ?? 'unknown'}${cachedForGuard?.name ? ` (${cachedForGuard.name})` : ''}`);
536
- console.log(`Parameter: ${parameter ?? '(none)'}`);
537
- console.log(`Risk level: ${riskLevel}`);
538
- if (reason)
539
- console.log(`Safety reason: ${reason}`);
540
- if (isDestructive)
541
- console.log(`Requires plan approval by default. ${destructiveExecutionHint()}`);
542
- console.log('(not executed — remove --explain to run)');
543
- }
544
- process.exit(0);
545
- }
546
- const destructive = isDestructiveCommand(cachedForGuard?.type, cmd, options.type);
547
- if (!isDryRun() && destructive && !options.yes && !allowsDirectDestructiveExecution()) {
548
- const typeLabel = cachedForGuard?.type ?? 'unknown';
549
- const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
550
- exitWithError({
551
- kind: 'guard',
552
- message: `Direct destructive execution disabled — destructive command "${cmd}" on ${typeLabel}.`,
553
- hint: reason ? `${destructiveExecutionHint()} Reason: ${reason}` : destructiveExecutionHint(),
554
- context: {
555
- command: cmd,
556
- deviceType: typeLabel,
557
- deviceId,
558
- directExecutionAllowed: false,
559
- requiredWorkflow: 'plan-approval',
560
- ...(reason ? { safetyReason: reason, destructiveReason: reason } : {}),
561
- },
562
- });
563
- }
564
- if (!options.yes && !isDryRun() && destructive) {
565
- const typeLabel = cachedForGuard?.type ?? 'unknown';
566
- const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
567
- exitWithError({
568
- kind: 'guard',
569
- message: `Refusing to run destructive command "${cmd}" on ${typeLabel} without --yes.`,
570
- hint: reason
571
- ? `Re-run with --yes only from an explicit dev profile, or use the reviewed plan flow. Reason: ${reason}`
572
- : `Re-run with --yes only from an explicit dev profile, use the reviewed plan flow, or --dry-run to preview without sending.`,
573
- context: { command: cmd, deviceType: typeLabel, deviceId, ...(reason ? { safetyReason: reason, destructiveReason: reason } : {}) },
574
- });
575
- }
576
- // Warn when --yes is given but the command is not destructive (no-op flag)
577
- if (options.yes && !destructive && !isDryRun()) {
578
- console.error(`Note: --yes has no effect; "${cmd}" is not a destructive command.`);
579
- }
580
- // parameter may be a JSON object string (e.g. S10 startClean) or a plain string
581
- let parsedParam = parameter ?? 'default';
582
- if (parameter) {
583
- try {
584
- parsedParam = JSON.parse(parameter);
585
- }
586
- catch {
587
- // keep as string
588
- }
589
- }
590
- // Capture for DryRunSignal catch branch (which runs after executeCommand throws).
591
- _cmd = cmd;
592
- _parsedParam = parsedParam;
593
- const body = await executeCommand(deviceId, cmd, parsedParam, options.type, undefined, { idempotencyKey: options.idempotencyKey });
594
- const isIr = getCachedDevice(deviceId)?.category === 'ir';
595
- const verification = isIr
596
- ? {
597
- verifiable: false,
598
- reason: 'IR transmission is unidirectional; no receipt acknowledgment is possible.',
599
- suggestedFollowup: 'Confirm visible change manually or via a paired state sensor.',
600
- }
601
- : null;
602
- if (isJsonMode()) {
603
- const result = { ok: true, command: cmd, deviceId };
604
- if (isIr) {
605
- result.subKind = 'ir-no-feedback';
606
- result.verification = verification;
607
- }
608
- if (body && typeof body === 'object' && Object.keys(body).length > 0) {
609
- Object.assign(result, body);
610
- }
611
- printJson(result);
612
- return;
613
- }
614
- if (isIr) {
615
- console.log(`→ IR signal sent: ${cmd} (no feedback — fire-and-forget)`);
616
- console.error('⚠ IR (unverifiable) — no receipt acknowledgment. Confirm state manually.');
617
- }
618
- else {
619
- console.log(`✓ Command sent: ${cmd}`);
620
- if (body && typeof body === 'object' && Object.keys(body).length > 0) {
621
- printKeyValue(body);
622
- }
623
- }
624
- }
625
- catch (error) {
626
- // Re-throw mock process.exit signals (Vitest intercepts process.exit as thrown
627
- // Error('__exit__')) so they aren't double-handled and the exit code is preserved.
628
- if (error instanceof Error && error.message === '__exit__')
629
- throw error;
630
- if (error instanceof DryRunSignal) {
631
- const commandType = (options.type ?? 'command');
632
- const wouldSend = { deviceId: _deviceId, command: _cmd, parameter: _parsedParam, commandType };
633
- if (isJsonMode()) {
634
- printJson({ dryRun: true, wouldSend });
635
- }
636
- else {
637
- console.log(`[dry-run] Would POST devices/${_deviceId}/commands with ${JSON.stringify({ command: _cmd, parameter: _parsedParam, commandType })}`);
638
- }
639
- return;
640
- }
641
- handleError(error);
642
- }
643
- });
644
- // switchbot devices types
645
- devices
646
- .command('types')
647
- .description('List all device types known to this CLI (offline reference, no API call)')
648
- .addHelpText('after', `
649
- Output columns: type, category (physical | ir), commands, aliases
650
- Use 'switchbot devices commands <type>' to see what a given type supports.
651
-
652
- Examples:
653
- $ switchbot devices types
654
- $ switchbot devices types --json
655
- `)
656
- .action(() => {
657
- try {
658
- const catalog = getEffectiveCatalog();
659
- const fmt = resolveFormat();
660
- if (fmt === 'json') {
661
- printJson(catalog);
662
- return;
663
- }
664
- const headers = ['type', 'role', 'category', 'commands', 'aliases'];
665
- const rows = catalog.map((e) => [
666
- e.type,
667
- e.role ?? '—',
668
- e.category,
669
- String(e.commands.length),
670
- (e.aliases ?? []).join(', ') || '—',
671
- ]);
672
- renderRows(headers, rows, fmt, resolveFields());
673
- if (fmt === 'table') {
674
- console.log(`\nTotal: ${catalog.length} device type(s)`);
675
- }
676
- }
677
- catch (error) {
678
- handleError(error);
679
- }
680
- });
681
- // switchbot devices commands <type>
682
- devices
683
- .command('commands')
684
- .description('Show supported commands, parameter formats, and status fields for a device type')
685
- .argument('<type...>', 'Device type name or alias (case-insensitive, partial matches supported; multi-word types do not need quoting)')
686
- .addHelpText('after', `
687
- This is the authoritative per-device reference — every command the CLI
688
- can send to a given type, its parameter format, and the status fields
689
- 'devices status' will return. Runs fully offline (no API call).
690
-
691
- Multi-word types can be passed either quoted or unquoted — both work:
692
- $ switchbot devices commands "Air Conditioner"
693
- $ switchbot devices commands Air Conditioner
694
- $ switchbot devices commands "Smart Lock"
695
-
696
- Examples:
697
- $ switchbot devices commands Bot
698
- $ switchbot devices commands curtain
699
- $ switchbot devices commands Robot --json
700
- `)
701
- .action((typeParts) => {
702
- try {
703
- // First try the joined form so legacy multi-word unquoted input still
704
- // works (`devices commands Air Conditioner` → "Air Conditioner"). If
705
- // that doesn't match and every individual token resolves on its own,
706
- // treat it as variadic and emit a section per type.
707
- const joined = typeParts.join(' ');
708
- const joinedMatch = findCatalogEntry(joined);
709
- if (joinedMatch && !Array.isArray(joinedMatch)) {
710
- if (isJsonMode()) {
711
- printJson(normalizeCatalogForJson(joinedMatch));
712
- }
713
- else {
714
- renderCatalogEntry(joinedMatch);
715
- }
716
- return;
717
- }
718
- if (typeParts.length > 1) {
719
- const individualMatches = [];
720
- for (const t of typeParts) {
721
- const m = findCatalogEntry(t);
722
- if (!m || Array.isArray(m)) {
723
- individualMatches.length = 0;
724
- break;
725
- }
726
- individualMatches.push(m);
727
- }
728
- if (individualMatches.length === typeParts.length) {
729
- if (isJsonMode()) {
730
- printJson(individualMatches.map(normalizeCatalogForJson));
731
- }
732
- else {
733
- individualMatches.forEach((entry, i) => {
734
- if (i > 0)
735
- console.log('');
736
- renderCatalogEntry(entry);
737
- });
738
- }
739
- return;
740
- }
741
- }
742
- if (!joinedMatch) {
743
- throw new UsageError(`No device type matches "${joined}". Try 'switchbot devices types' to see the full list.`);
744
- }
745
- // joinedMatch is an ambiguous-match array here
746
- const types = joinedMatch.map((m) => m.type).join(', ');
747
- throw new UsageError(`"${joined}" matches multiple types: ${types}. Be more specific.`);
748
- }
749
- catch (error) {
750
- handleError(error);
751
- }
752
- });
753
- // switchbot devices describe <deviceId>
754
- devices
755
- .command('describe')
756
- .description('Describe a device by ID: metadata + supported commands + status fields (1 API call)')
757
- .argument('[deviceId]', 'Target device ID (or use --name)')
758
- .option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
759
- .option('--name-strategy <s>', `Name match strategy: ${ALL_STRATEGIES.join('|')} (default: fuzzy)`, stringArg('--name-strategy'))
760
- .option('--name-type <type>', 'Narrow --name by device type', stringArg('--name-type'))
761
- .option('--name-category <cat>', 'Narrow --name by category: physical|ir', enumArg('--name-category', ['physical', 'ir']))
762
- .option('--name-room <room>', 'Narrow --name by room name (substring match)', stringArg('--name-room'))
763
- .option('--live', 'Also fetch live status values and merge them into capabilities (costs 1 extra API call)')
764
- .addHelpText('after', `
765
- Makes a GET /v1.1/devices call to look up the device's type, then prints its
766
- metadata alongside the matching catalog entry (supported commands + parameter
767
- formats + status field names). With --live, makes a second call to fetch the
768
- current status values and merges them into the output.
769
-
770
- JSON output shape (--json):
771
- {
772
- device: <raw API fields>,
773
- controlType: <string|null>,
774
- catalog: <catalog entry, or null>,
775
- capabilities: {
776
- role: <functional role>,
777
- readOnly: <boolean>,
778
- commands: [{command, parameter, description, idempotent?, destructive?, exampleParams?}],
779
- statusFields: [<name>],
780
- liveStatus: <status payload when --live was passed>
781
- },
782
- source: "catalog" | "live" | "catalog+live" | "none",
783
- suggestedActions: [{command, parameter?, description}],
784
- expandHint?: {command, flags, example} // present when the type supports 'devices expand'
785
- }
786
-
787
- Examples:
788
- $ switchbot devices describe ABC123DEF456
789
- $ switchbot devices describe ABC123DEF456 --live
790
- $ switchbot devices describe ABC123DEF456 --json
791
- $ switchbot devices describe <lockId> --json | jq '.capabilities.commands[] | select(.destructive)'
792
- `)
793
- .action(async (deviceIdArg, options) => {
794
- try {
795
- const deviceId = resolveDeviceId(deviceIdArg, options.name, {
796
- strategy: options.nameStrategy ?? 'fuzzy',
797
- type: options.nameType,
798
- category: options.nameCategory,
799
- room: options.nameRoom,
800
- });
801
- const result = await describeDevice(deviceId, options);
802
- const { device, isPhysical, typeName, controlType, catalog, capabilities, source, suggestedActions: picks } = result;
803
- if (isJsonMode()) {
804
- const expandHint = catalog ? EXPAND_HINTS[catalog.type] : undefined;
805
- printJson({
806
- device,
807
- controlType,
808
- catalog,
809
- capabilities,
810
- source,
811
- suggestedActions: picks,
812
- ...(expandHint ? { expandHint: { command: expandHint.command, flags: expandHint.flags, example: `switchbot devices expand ${deviceId} ${expandHint.command} ${expandHint.flags}` } } : {}),
813
- });
814
- return;
815
- }
816
- if (isPhysical) {
817
- const physical = device;
818
- printKeyValue({
819
- deviceId: physical.deviceId,
820
- deviceName: physical.deviceName,
821
- deviceType: physical.deviceType || '—',
822
- controlType: physical.controlType || '—',
823
- family: physical.familyName || '—',
824
- roomID: physical.roomID || '—',
825
- room: physical.roomName || '—',
826
- hub: !physical.hubDeviceId || physical.hubDeviceId === '000000000000' ? '—' : physical.hubDeviceId,
827
- cloudService: physical.enableCloudService,
828
- });
829
- }
830
- else {
831
- const ir = device;
832
- const inherited = result.inheritedLocation;
833
- printKeyValue({
834
- deviceId: ir.deviceId,
835
- deviceName: ir.deviceName,
836
- remoteType: ir.remoteType,
837
- controlType: ir.controlType || '—',
838
- family: inherited?.family || '—',
839
- roomID: inherited?.roomID || '—',
840
- room: inherited?.room || '—',
841
- hub: ir.hubDeviceId || '—',
842
- });
843
- }
844
- const liveStatus = capabilities && 'liveStatus' in capabilities ? capabilities.liveStatus : undefined;
845
- console.log('');
846
- if (!catalog) {
847
- console.log(`(Type "${typeName}" is not in the built-in catalog — no command reference available.)`);
848
- if (isPhysical) {
849
- console.log(`Try 'switchbot devices status ${deviceId}' to see what this device reports.`);
850
- }
851
- else {
852
- console.log(`Send custom IR buttons with: switchbot devices command ${deviceId} "<buttonName>" --type customize`);
853
- }
854
- if (liveStatus) {
855
- console.log('\nLive status:');
856
- printKeyValue(liveStatus);
857
- }
858
- return;
859
- }
860
- renderCatalogEntry(catalog);
861
- if (liveStatus) {
862
- console.log('\nLive status:');
863
- printKeyValue(liveStatus);
864
- }
865
- }
866
- catch (error) {
867
- if (error instanceof DeviceNotFoundError) {
868
- const message = `${error.message} Try 'switchbot devices list' to see the full list.`;
869
- exitWithError({
870
- code: 1,
871
- kind: 'runtime',
872
- message,
873
- extra: { errorClass: 'runtime', transient: false },
874
- });
875
- }
876
- handleError(error);
877
- }
878
- });
879
- // switchbot devices batch <command> ...
880
- registerBatchCommand(devices);
881
- // switchbot devices watch <id...>
882
- registerWatchCommand(devices);
883
- // switchbot devices explain <id>
884
- registerExplainCommand(devices);
885
- // switchbot devices expand <id> <cmd> [semantic flags]
886
- registerExpandCommand(devices);
887
- // switchbot devices meta set/get/list/clear
888
- registerDevicesMetaCommand(devices);
889
- }
890
- function normalizeCatalogForJson(entry) {
891
- return {
892
- ...entry,
893
- commands: entry.commands.map((c) => {
894
- const tier = deriveSafetyTier(c, entry);
895
- const reason = getCommandSafetyReason(c);
896
- return {
897
- ...c,
898
- safetyTier: tier,
899
- ...(reason ? { safetyReason: reason } : {}),
900
- };
901
- }),
902
- };
903
- }
904
- function renderCatalogEntry(entry) {
905
- console.log(`Type: ${entry.type}`);
906
- console.log(`Category: ${entry.category === 'ir' ? 'IR remote' : 'Physical device'}`);
907
- if (entry.role)
908
- console.log(`Role: ${entry.role}`);
909
- if (entry.readOnly)
910
- console.log(`ReadOnly: yes (status-only device, no control commands)`);
911
- if (entry.aliases && entry.aliases.length > 0) {
912
- console.log(`Aliases: ${entry.aliases.join(', ')}`);
913
- }
914
- if (entry.commands.length === 0) {
915
- console.log('\nCommands: (none — status-only device)');
916
- }
917
- else {
918
- console.log('\nCommands:');
919
- const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
920
- const rows = entry.commands.map((c) => {
921
- const tier = deriveSafetyTier(c, entry);
922
- const flags = [];
923
- if (c.commandType === 'customize')
924
- flags.push('customize');
925
- if (tier === 'destructive')
926
- flags.push('!destructive');
927
- const label = flags.length > 0 ? `${c.command} [${flags.join(', ')}]` : c.command;
928
- const base = [label, c.parameter, c.description];
929
- return hasExamples ? [...base, (c.exampleParams ?? []).join(' | ') || ''] : base;
930
- });
931
- const tableHeaders = hasExamples
932
- ? ['command', 'parameter', 'description', 'example']
933
- : ['command', 'parameter', 'description'];
934
- printTable(tableHeaders, rows);
935
- const hasDestructive = entry.commands.some((c) => deriveSafetyTier(c, entry) === 'destructive');
936
- if (hasDestructive) {
937
- console.log('\n[!destructive] commands have hard-to-reverse real-world effects — confirm before issuing.');
938
- }
939
- }
940
- if (entry.statusFields && entry.statusFields.length > 0) {
941
- console.log('\nStatus fields (from "devices status"):');
942
- console.log(' ' + entry.statusFields.join(', '));
943
- }
944
- const expandHint = EXPAND_HINTS[entry.type];
945
- if (expandHint) {
946
- console.log(`\nTip: Use 'devices expand <id> ${expandHint.command} ${expandHint.flags}' for semantic flags instead of raw parameters.`);
947
- }
948
- }
949
- //# sourceMappingURL=devices.js.map