@switchbot/openapi-cli 2.2.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 +3 -0
- package/dist/commands/batch.js +7 -5
- package/dist/commands/cache.js +3 -1
- 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 +13 -7
- package/dist/commands/events.js +30 -17
- package/dist/commands/expand.js +10 -9
- package/dist/commands/history.js +4 -3
- package/dist/commands/mcp.js +6 -5
- package/dist/commands/schema.js +6 -3
- package/dist/commands/watch.js +4 -3
- package/dist/commands/webhook.js +2 -1
- package/dist/config.js +7 -2
- package/dist/index.js +48 -19
- package/dist/utils/arg-parsers.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,6 +177,8 @@ switchbot --help
|
|
|
177
177
|
switchbot devices command --help
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
> **Tip — required-value flags and subcommands.** Flags like `--profile`, `--timeout`, `--max`, and `--interval` take a value. If you omit it, Commander will happily consume the next token — including a subcommand name. Since v2.2.1 the CLI rejects that eagerly (exit 2 with a clear error), but if you ever hit `unknown command 'list'` after something like `switchbot --profile list`, use the `--flag=value` form: `switchbot --profile=home devices list`.
|
|
181
|
+
|
|
180
182
|
### `--dry-run`
|
|
181
183
|
|
|
182
184
|
Intercepts every non-GET request: the CLI prints the URL/body it would have
|
|
@@ -206,6 +208,7 @@ switchbot config list-profiles # List saved profiles
|
|
|
206
208
|
# Default columns (4): deviceId, deviceName, type, category
|
|
207
209
|
# Pass --wide for the full 10-column operator view
|
|
208
210
|
switchbot devices list
|
|
211
|
+
switchbot devices ls # short alias for 'list'
|
|
209
212
|
switchbot devices list --wide
|
|
210
213
|
switchbot devices list --json | jq '.deviceList[].deviceId'
|
|
211
214
|
|
package/dist/commands/batch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { intArg, enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printJson, isJsonMode, handleError, buildErrorPayload } from '../utils/output.js';
|
|
2
3
|
import { fetchDeviceList, executeCommand, isDestructiveCommand, buildHubLocationMap, } from '../lib/devices.js';
|
|
3
4
|
import { createClient } from '../api/client.js';
|
|
@@ -6,6 +7,7 @@ import { isDryRun } from '../utils/flags.js';
|
|
|
6
7
|
import { DryRunSignal } from '../api/client.js';
|
|
7
8
|
import { getCachedTypeMap } from '../devices/cache.js';
|
|
8
9
|
const DEFAULT_CONCURRENCY = 5;
|
|
10
|
+
const COMMAND_TYPES = ['command', 'customize'];
|
|
9
11
|
/** Run `task(x)` for every element with at most `concurrency` running at once. */
|
|
10
12
|
async function runPool(items, concurrency, task) {
|
|
11
13
|
const results = new Array(items.length);
|
|
@@ -84,13 +86,13 @@ export function registerBatchCommand(devices) {
|
|
|
84
86
|
.description('Send the same command to many devices in one run (filter- or stdin-driven)')
|
|
85
87
|
.argument('<command>', 'Command name, e.g. turnOn, turnOff, setBrightness')
|
|
86
88
|
.argument('[parameter]', 'Command parameter (same rules as `devices command`; omit for no-arg)')
|
|
87
|
-
.option('--filter <expr>', 'Target devices matching a filter, e.g. type=Bot,family=Home')
|
|
88
|
-
.option('--ids <csv>', 'Explicit comma-separated list of deviceIds')
|
|
89
|
-
.option('--concurrency <n>', 'Max parallel in-flight requests (default 5)', '5')
|
|
89
|
+
.option('--filter <expr>', 'Target devices matching a filter, e.g. type=Bot,family=Home', stringArg('--filter'))
|
|
90
|
+
.option('--ids <csv>', 'Explicit comma-separated list of deviceIds', stringArg('--ids'))
|
|
91
|
+
.option('--concurrency <n>', 'Max parallel in-flight requests (default 5)', intArg('--concurrency', { min: 1 }), '5')
|
|
90
92
|
.option('--yes', 'Allow destructive commands (Smart Lock unlock, garage open, ...)')
|
|
91
|
-
.option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', 'command')
|
|
93
|
+
.option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
|
|
92
94
|
.option('--stdin', 'Read deviceIds from stdin, one per line (same as trailing "-")')
|
|
93
|
-
.option('--idempotency-key-prefix <prefix>', 'Prefix for idempotency keys (key per device: <prefix>-<deviceId>)')
|
|
95
|
+
.option('--idempotency-key-prefix <prefix>', 'Prefix for idempotency keys (key per device: <prefix>-<deviceId>)', stringArg('--idempotency-key-prefix'))
|
|
94
96
|
.addHelpText('after', `
|
|
95
97
|
Targets are resolved in this priority order:
|
|
96
98
|
1. --ids when present (explicit deviceIds)
|
package/dist/commands/cache.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enumArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
3
|
import { clearCache, clearStatusCache, describeCache, loadStatusCache, } from '../devices/cache.js';
|
|
3
4
|
function formatAge(ms) {
|
|
@@ -15,6 +16,7 @@ function formatAge(ms) {
|
|
|
15
16
|
return `${h}h ${m % 60}m`;
|
|
16
17
|
}
|
|
17
18
|
export function registerCacheCommand(program) {
|
|
19
|
+
const CACHE_KEYS = ['list', 'status', 'all'];
|
|
18
20
|
const cache = program
|
|
19
21
|
.command('cache')
|
|
20
22
|
.description('Inspect and manage the local SwitchBot CLI caches')
|
|
@@ -78,7 +80,7 @@ Examples:
|
|
|
78
80
|
cache
|
|
79
81
|
.command('clear')
|
|
80
82
|
.description('Delete cache files')
|
|
81
|
-
.option('--key <which>', 'Which cache to clear: "list" | "status" | "all" (default)', 'all')
|
|
83
|
+
.option('--key <which>', 'Which cache to clear: "list" | "status" | "all" (default)', enumArg('--key', CACHE_KEYS), 'all')
|
|
82
84
|
.action((options) => {
|
|
83
85
|
try {
|
|
84
86
|
const key = options.key;
|
package/dist/commands/catalog.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { enumArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printTable, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
3
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
4
|
import { DEVICE_CATALOG, findCatalogEntry, getCatalogOverlayPath, getEffectiveCatalog, loadCatalogOverlay, resetCatalogOverlayCache, } from '../devices/catalog.js';
|
|
4
5
|
export function registerCatalogCommand(program) {
|
|
6
|
+
const SOURCES = ['built-in', 'overlay', 'effective'];
|
|
5
7
|
const catalog = program
|
|
6
8
|
.command('catalog')
|
|
7
9
|
.description('Inspect the built-in device catalog and any local overlay')
|
|
@@ -66,7 +68,7 @@ Examples:
|
|
|
66
68
|
.command('show')
|
|
67
69
|
.description("Show the effective catalog (or one entry). Defaults to 'effective' source.")
|
|
68
70
|
.argument('[type...]', 'Optional device type/alias (case-insensitive, partial match)')
|
|
69
|
-
.option('--source <source>', 'Which catalog to show: built-in | overlay | effective (default)', 'effective')
|
|
71
|
+
.option('--source <source>', 'Which catalog to show: built-in | overlay | effective (default)', enumArg('--source', SOURCES), 'effective')
|
|
70
72
|
.action((typeParts, options) => {
|
|
71
73
|
try {
|
|
72
74
|
const source = options.source;
|
|
@@ -12,13 +12,19 @@ _switchbot_completion() {
|
|
|
12
12
|
cword="\${COMP_CWORD}"
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
local top_cmds="config devices scenes webhook completion help"
|
|
16
|
-
local config_sub="set-token show"
|
|
17
|
-
local devices_sub="list status command types commands"
|
|
15
|
+
local top_cmds="config devices scenes webhook completion mcp quota catalog cache events doctor schema history plan capabilities help"
|
|
16
|
+
local config_sub="set-token show list-profiles"
|
|
17
|
+
local devices_sub="list ls status command types commands describe batch watch explain expand meta"
|
|
18
18
|
local scenes_sub="list execute"
|
|
19
19
|
local webhook_sub="setup query update delete"
|
|
20
|
+
local events_sub="tail mqtt-tail"
|
|
21
|
+
local quota_sub="status reset"
|
|
22
|
+
local catalog_sub="path show diff refresh"
|
|
23
|
+
local cache_sub="show clear"
|
|
24
|
+
local history_sub="show replay"
|
|
25
|
+
local plan_sub="schema validate run"
|
|
20
26
|
local completion_shells="bash zsh fish powershell"
|
|
21
|
-
local global_opts="--json --verbose -v --dry-run --timeout --config --help -h --version -V"
|
|
27
|
+
local global_opts="--json --format --fields --verbose -v --dry-run --timeout --retry-on-429 --backoff --no-retry --no-quota --cache --no-cache --config --profile --audit-log --audit-log-path --help -h --version -V"
|
|
22
28
|
|
|
23
29
|
if [[ \${cword} -eq 1 ]]; then
|
|
24
30
|
COMPREPLY=( $(compgen -W "\${top_cmds} \${global_opts}" -- "\${cur}") )
|
|
@@ -43,6 +49,36 @@ _switchbot_completion() {
|
|
|
43
49
|
COMPREPLY=( $(compgen -W "\${scenes_sub}" -- "\${cur}") )
|
|
44
50
|
fi
|
|
45
51
|
;;
|
|
52
|
+
events)
|
|
53
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
54
|
+
COMPREPLY=( $(compgen -W "\${events_sub}" -- "\${cur}") )
|
|
55
|
+
fi
|
|
56
|
+
;;
|
|
57
|
+
quota)
|
|
58
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
59
|
+
COMPREPLY=( $(compgen -W "\${quota_sub}" -- "\${cur}") )
|
|
60
|
+
fi
|
|
61
|
+
;;
|
|
62
|
+
catalog)
|
|
63
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
64
|
+
COMPREPLY=( $(compgen -W "\${catalog_sub}" -- "\${cur}") )
|
|
65
|
+
fi
|
|
66
|
+
;;
|
|
67
|
+
cache)
|
|
68
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
69
|
+
COMPREPLY=( $(compgen -W "\${cache_sub}" -- "\${cur}") )
|
|
70
|
+
fi
|
|
71
|
+
;;
|
|
72
|
+
history)
|
|
73
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
74
|
+
COMPREPLY=( $(compgen -W "\${history_sub}" -- "\${cur}") )
|
|
75
|
+
fi
|
|
76
|
+
;;
|
|
77
|
+
plan)
|
|
78
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
79
|
+
COMPREPLY=( $(compgen -W "\${plan_sub}" -- "\${cur}") )
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
46
82
|
webhook)
|
|
47
83
|
if [[ \${cword} -eq 2 ]]; then
|
|
48
84
|
COMPREPLY=( $(compgen -W "\${webhook_sub}" -- "\${cur}") )
|
|
@@ -69,22 +105,39 @@ const ZSH_SCRIPT = `# switchbot zsh completion
|
|
|
69
105
|
# source <(switchbot completion zsh)
|
|
70
106
|
|
|
71
107
|
_switchbot() {
|
|
72
|
-
local -a top_cmds config_sub devices_sub scenes_sub webhook_sub completion_shells
|
|
108
|
+
local -a top_cmds config_sub devices_sub scenes_sub webhook_sub events_sub quota_sub catalog_sub cache_sub history_sub plan_sub completion_shells
|
|
73
109
|
top_cmds=(
|
|
74
110
|
'config:Manage API credentials'
|
|
75
111
|
'devices:List and control devices'
|
|
76
112
|
'scenes:List and execute scenes'
|
|
77
113
|
'webhook:Manage webhook configuration'
|
|
78
114
|
'completion:Print a shell completion script'
|
|
115
|
+
'mcp:Run the MCP server'
|
|
116
|
+
'quota:Inspect local request quota'
|
|
117
|
+
'catalog:Inspect the built-in device catalog'
|
|
118
|
+
'cache:Inspect local caches'
|
|
119
|
+
'events:Receive webhook or MQTT events'
|
|
120
|
+
'doctor:Run self-checks'
|
|
121
|
+
'schema:Export the device catalog as JSON'
|
|
122
|
+
'history:View and replay audited commands'
|
|
123
|
+
'plan:Validate and run batch plans'
|
|
124
|
+
'capabilities:Print a machine-readable manifest'
|
|
79
125
|
'help:Show help for a command'
|
|
80
126
|
)
|
|
81
|
-
config_sub=('set-token:Save token + secret' 'show:Show current credential source')
|
|
127
|
+
config_sub=('set-token:Save token + secret' 'show:Show current credential source' 'list-profiles:List named credential profiles')
|
|
82
128
|
devices_sub=(
|
|
83
129
|
'list:List all devices'
|
|
130
|
+
'ls:Alias for list'
|
|
84
131
|
'status:Query device status'
|
|
85
132
|
'command:Send a control command'
|
|
86
133
|
'types:List known device types (offline)'
|
|
87
134
|
'commands:Show commands for a device type (offline)'
|
|
135
|
+
'describe:Show metadata + supported commands for one device'
|
|
136
|
+
'batch:Send one command to many devices'
|
|
137
|
+
'watch:Poll device status and emit changes'
|
|
138
|
+
'explain:One-shot device summary'
|
|
139
|
+
'expand:Build wire-format params from semantic flags'
|
|
140
|
+
'meta:Manage local device metadata'
|
|
88
141
|
)
|
|
89
142
|
scenes_sub=('list:List manual scenes' 'execute:Run a scene')
|
|
90
143
|
webhook_sub=(
|
|
@@ -93,15 +146,32 @@ _switchbot() {
|
|
|
93
146
|
'update:Enable/disable a webhook'
|
|
94
147
|
'delete:Delete a webhook'
|
|
95
148
|
)
|
|
149
|
+
events_sub=('tail:Run a local webhook receiver' 'mqtt-tail:Stream MQTT shadow events')
|
|
150
|
+
quota_sub=('status:Show today and recent quota usage' 'reset:Delete the local quota counter')
|
|
151
|
+
catalog_sub=('path:Show overlay path' 'show:Show built-in/overlay/effective catalog' 'diff:Show overlay changes' 'refresh:Clear overlay cache')
|
|
152
|
+
cache_sub=('show:Summarize cache files' 'clear:Delete cache files')
|
|
153
|
+
history_sub=('show:Print recent audit entries' 'replay:Re-run one audited command')
|
|
154
|
+
plan_sub=('schema:Print the plan schema' 'validate:Validate a plan file' 'run:Validate and execute a plan')
|
|
96
155
|
completion_shells=('bash' 'zsh' 'fish' 'powershell')
|
|
97
156
|
|
|
98
157
|
local global_opts
|
|
99
158
|
global_opts=(
|
|
100
159
|
'--json[Raw JSON output]'
|
|
160
|
+
'--format[Output format]:type:(table json jsonl tsv yaml id)'
|
|
161
|
+
'--fields[Comma-separated output columns]:csv:'
|
|
101
162
|
'(-v --verbose)'{-v,--verbose}'[Log HTTP details to stderr]'
|
|
102
163
|
'--dry-run[Print mutating requests without sending]'
|
|
103
164
|
'--timeout[HTTP timeout in ms]:ms:'
|
|
165
|
+
'--retry-on-429[Max 429 retries]:n:'
|
|
166
|
+
'--backoff[Retry backoff strategy]:strategy:(linear exponential)'
|
|
167
|
+
'--no-retry[Disable 429 retries]'
|
|
168
|
+
'--no-quota[Disable the local quota counter]'
|
|
169
|
+
'--cache[Cache mode]:mode:'
|
|
170
|
+
'--no-cache[Disable cache reads]'
|
|
104
171
|
'--config[Override credential file path]:path:_files'
|
|
172
|
+
'--profile[Use a named credential profile]:name:'
|
|
173
|
+
'--audit-log[Append mutating commands to ~/.switchbot/audit.log]'
|
|
174
|
+
'--audit-log-path[Custom audit log file path]:path:_files'
|
|
105
175
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
106
176
|
'(-V --version)'{-V,--version}'[Show version]'
|
|
107
177
|
)
|
|
@@ -122,6 +192,12 @@ _switchbot() {
|
|
|
122
192
|
devices) _describe 'devices' devices_sub ;;
|
|
123
193
|
scenes) _describe 'scenes' scenes_sub ;;
|
|
124
194
|
webhook) _describe 'webhook' webhook_sub ;;
|
|
195
|
+
events) _describe 'events' events_sub ;;
|
|
196
|
+
quota) _describe 'quota' quota_sub ;;
|
|
197
|
+
catalog) _describe 'catalog' catalog_sub ;;
|
|
198
|
+
cache) _describe 'cache' cache_sub ;;
|
|
199
|
+
history) _describe 'history' history_sub ;;
|
|
200
|
+
plan) _describe 'plan' plan_sub ;;
|
|
125
201
|
completion) _values 'shell' $completion_shells ;;
|
|
126
202
|
esac
|
|
127
203
|
;;
|
|
@@ -143,10 +219,21 @@ complete -c switchbot -f
|
|
|
143
219
|
|
|
144
220
|
# Global options
|
|
145
221
|
complete -c switchbot -l json -d 'Raw JSON output'
|
|
222
|
+
complete -c switchbot -l format -r -d 'Output format'
|
|
223
|
+
complete -c switchbot -l fields -r -d 'Comma-separated output columns'
|
|
146
224
|
complete -c switchbot -s v -l verbose -d 'Log HTTP details to stderr'
|
|
147
225
|
complete -c switchbot -l dry-run -d 'Print mutating requests without sending'
|
|
148
226
|
complete -c switchbot -l timeout -r -d 'HTTP timeout in ms'
|
|
227
|
+
complete -c switchbot -l retry-on-429 -r -d 'Max 429 retries'
|
|
228
|
+
complete -c switchbot -l backoff -r -d 'Retry backoff strategy'
|
|
229
|
+
complete -c switchbot -l no-retry -d 'Disable 429 retries'
|
|
230
|
+
complete -c switchbot -l no-quota -d 'Disable the local quota counter'
|
|
231
|
+
complete -c switchbot -l cache -r -d 'Cache mode'
|
|
232
|
+
complete -c switchbot -l no-cache -d 'Disable cache reads'
|
|
149
233
|
complete -c switchbot -l config -r -d 'Credential file path'
|
|
234
|
+
complete -c switchbot -l profile -r -d 'Named credential profile'
|
|
235
|
+
complete -c switchbot -l audit-log -d 'Append mutating commands to audit log'
|
|
236
|
+
complete -c switchbot -l audit-log-path -r -d 'Custom audit log file path'
|
|
150
237
|
complete -c switchbot -s h -l help -d 'Show help'
|
|
151
238
|
complete -c switchbot -s V -l version -d 'Show version'
|
|
152
239
|
|
|
@@ -156,13 +243,23 @@ complete -c switchbot -n '__fish_use_subcommand' -a 'devices' -d 'List and co
|
|
|
156
243
|
complete -c switchbot -n '__fish_use_subcommand' -a 'scenes' -d 'List and execute scenes'
|
|
157
244
|
complete -c switchbot -n '__fish_use_subcommand' -a 'webhook' -d 'Manage webhook configuration'
|
|
158
245
|
complete -c switchbot -n '__fish_use_subcommand' -a 'completion' -d 'Print a shell completion script'
|
|
246
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'mcp' -d 'Run the MCP server'
|
|
247
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'quota' -d 'Inspect local request quota'
|
|
248
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'catalog' -d 'Inspect the built-in device catalog'
|
|
249
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'cache' -d 'Inspect local caches'
|
|
250
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'events' -d 'Receive webhook or MQTT events'
|
|
251
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'doctor' -d 'Run self-checks'
|
|
252
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'schema' -d 'Export the device catalog as JSON'
|
|
253
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'history' -d 'View and replay audited commands'
|
|
254
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'plan' -d 'Validate and run batch plans'
|
|
255
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'capabilities' -d 'Print a machine-readable manifest'
|
|
159
256
|
complete -c switchbot -n '__fish_use_subcommand' -a 'help' -d 'Show help'
|
|
160
257
|
|
|
161
258
|
# config
|
|
162
|
-
complete -c switchbot -n '__fish_seen_subcommand_from config' -a 'set-token show'
|
|
259
|
+
complete -c switchbot -n '__fish_seen_subcommand_from config' -a 'set-token show list-profiles'
|
|
163
260
|
|
|
164
261
|
# devices
|
|
165
|
-
complete -c switchbot -n '__fish_seen_subcommand_from devices' -a 'list status command types commands'
|
|
262
|
+
complete -c switchbot -n '__fish_seen_subcommand_from devices' -a 'list ls status command types commands describe batch watch explain expand meta'
|
|
166
263
|
|
|
167
264
|
# scenes
|
|
168
265
|
complete -c switchbot -n '__fish_seen_subcommand_from scenes' -a 'list execute'
|
|
@@ -172,6 +269,24 @@ complete -c switchbot -n '__fish_seen_subcommand_from webhook' -a 'setup query u
|
|
|
172
269
|
complete -c switchbot -n '__fish_seen_subcommand_from webhook; and __fish_seen_subcommand_from update' -l enable -d 'Enable the webhook'
|
|
173
270
|
complete -c switchbot -n '__fish_seen_subcommand_from webhook; and __fish_seen_subcommand_from update' -l disable -d 'Disable the webhook'
|
|
174
271
|
|
|
272
|
+
# events
|
|
273
|
+
complete -c switchbot -n '__fish_seen_subcommand_from events' -a 'tail mqtt-tail'
|
|
274
|
+
|
|
275
|
+
# quota
|
|
276
|
+
complete -c switchbot -n '__fish_seen_subcommand_from quota' -a 'status reset'
|
|
277
|
+
|
|
278
|
+
# catalog
|
|
279
|
+
complete -c switchbot -n '__fish_seen_subcommand_from catalog' -a 'path show diff refresh'
|
|
280
|
+
|
|
281
|
+
# cache
|
|
282
|
+
complete -c switchbot -n '__fish_seen_subcommand_from cache' -a 'show clear'
|
|
283
|
+
|
|
284
|
+
# history
|
|
285
|
+
complete -c switchbot -n '__fish_seen_subcommand_from history' -a 'show replay'
|
|
286
|
+
|
|
287
|
+
# plan
|
|
288
|
+
complete -c switchbot -n '__fish_seen_subcommand_from plan' -a 'schema validate run'
|
|
289
|
+
|
|
175
290
|
# completion
|
|
176
291
|
complete -c switchbot -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish powershell'
|
|
177
292
|
`;
|
|
@@ -186,13 +301,19 @@ Register-ArgumentCompleter -Native -CommandName switchbot -ScriptBlock {
|
|
|
186
301
|
$tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
|
|
187
302
|
$count = $tokens.Count
|
|
188
303
|
|
|
189
|
-
$top = 'config','devices','scenes','webhook','completion','help'
|
|
190
|
-
$configSub = 'set-token','show'
|
|
191
|
-
$devicesSub = 'list','status','command','types','commands'
|
|
304
|
+
$top = 'config','devices','scenes','webhook','completion','mcp','quota','catalog','cache','events','doctor','schema','history','plan','capabilities','help'
|
|
305
|
+
$configSub = 'set-token','show','list-profiles'
|
|
306
|
+
$devicesSub = 'list','ls','status','command','types','commands','describe','batch','watch','explain','expand','meta'
|
|
192
307
|
$scenesSub = 'list','execute'
|
|
193
308
|
$webhookSub = 'setup','query','update','delete'
|
|
309
|
+
$eventsSub = 'tail','mqtt-tail'
|
|
310
|
+
$quotaSub = 'status','reset'
|
|
311
|
+
$catalogSub = 'path','show','diff','refresh'
|
|
312
|
+
$cacheSub = 'show','clear'
|
|
313
|
+
$historySub = 'show','replay'
|
|
314
|
+
$planSub = 'schema','validate','run'
|
|
194
315
|
$shells = 'bash','zsh','fish','powershell'
|
|
195
|
-
$globalOpts = '--json','--verbose','-v','--dry-run','--timeout','--config','--help','-h','--version','-V'
|
|
316
|
+
$globalOpts = '--json','--format','--fields','--verbose','-v','--dry-run','--timeout','--retry-on-429','--backoff','--no-retry','--no-quota','--cache','--no-cache','--config','--profile','--audit-log','--audit-log-path','--help','-h','--version','-V'
|
|
196
317
|
|
|
197
318
|
function _emit($values) {
|
|
198
319
|
$values |
|
|
@@ -206,6 +327,12 @@ Register-ArgumentCompleter -Native -CommandName switchbot -ScriptBlock {
|
|
|
206
327
|
'config' { if ($count -eq 3) { return _emit $configSub } }
|
|
207
328
|
'devices' { if ($count -eq 3) { return _emit $devicesSub } }
|
|
208
329
|
'scenes' { if ($count -eq 3) { return _emit $scenesSub } }
|
|
330
|
+
'events' { if ($count -eq 3) { return _emit $eventsSub } }
|
|
331
|
+
'quota' { if ($count -eq 3) { return _emit $quotaSub } }
|
|
332
|
+
'catalog' { if ($count -eq 3) { return _emit $catalogSub } }
|
|
333
|
+
'cache' { if ($count -eq 3) { return _emit $cacheSub } }
|
|
334
|
+
'history' { if ($count -eq 3) { return _emit $historySub } }
|
|
335
|
+
'plan' { if ($count -eq 3) { return _emit $planSub } }
|
|
209
336
|
'webhook' {
|
|
210
337
|
if ($count -eq 3) { return _emit $webhookSub }
|
|
211
338
|
if ($tokens[2] -eq 'update') { return _emit ('--enable','--disable' + $globalOpts) }
|
package/dist/commands/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { stringArg } from '../utils/arg-parsers.js';
|
|
3
4
|
import { saveConfig, showConfig, listProfiles } from '../config.js';
|
|
4
5
|
import { isJsonMode, printJson } from '../utils/output.js';
|
|
5
6
|
import chalk from 'chalk';
|
|
@@ -49,9 +50,9 @@ Obtain your token/secret from the SwitchBot mobile app:
|
|
|
49
50
|
.description('Save token and secret (mode 0600). Use --profile to target a named profile.')
|
|
50
51
|
.argument('[token]', 'API token; omit when using --from-env-file / --from-op')
|
|
51
52
|
.argument('[secret]', 'API client secret; omit when using --from-env-file / --from-op')
|
|
52
|
-
.option('--from-env-file <path>', 'Read SWITCHBOT_TOKEN and SWITCHBOT_SECRET from a dotenv file')
|
|
53
|
-
.option('--from-op <tokenRef>', 'Read token via 1Password CLI (op read). Pair with --op-secret <ref>')
|
|
54
|
-
.option('--op-secret <secretRef>', '1Password reference for the secret, used with --from-op')
|
|
53
|
+
.option('--from-env-file <path>', 'Read SWITCHBOT_TOKEN and SWITCHBOT_SECRET from a dotenv file', stringArg('--from-env-file'))
|
|
54
|
+
.option('--from-op <tokenRef>', 'Read token via 1Password CLI (op read). Pair with --op-secret <ref>', stringArg('--from-op'))
|
|
55
|
+
.option('--op-secret <secretRef>', '1Password reference for the secret, used with --from-op', stringArg('--op-secret'))
|
|
55
56
|
.addHelpText('after', `
|
|
56
57
|
Examples:
|
|
57
58
|
$ switchbot config set-token <token> <secret>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { handleError, isJsonMode, printJson, printTable, UsageError } from '../utils/output.js';
|
|
2
3
|
import { loadDeviceMeta, setDeviceMeta, clearDeviceMeta, getDeviceMeta, getMetaFilePath, } from '../devices/device-meta.js';
|
|
3
4
|
export function registerDevicesMetaCommand(devices) {
|
|
@@ -9,10 +10,10 @@ export function registerDevicesMetaCommand(devices) {
|
|
|
9
10
|
.command('set')
|
|
10
11
|
.description('Set local metadata for a device (alias, hide/show, notes)')
|
|
11
12
|
.argument('<deviceId>', 'Target device ID')
|
|
12
|
-
.option('--alias <name>', 'Local alias for the device (used with --name flag)')
|
|
13
|
+
.option('--alias <name>', 'Local alias for the device (used with --name flag)', stringArg('--alias'))
|
|
13
14
|
.option('--hide', 'Hide this device from "devices list"')
|
|
14
15
|
.option('--show', 'Un-hide this device')
|
|
15
|
-
.option('--notes <text>', 'Freeform notes shown in "devices describe"')
|
|
16
|
+
.option('--notes <text>', 'Freeform notes shown in "devices describe"', stringArg('--notes'))
|
|
16
17
|
.action((deviceId, options) => {
|
|
17
18
|
try {
|
|
18
19
|
if (options.hide && options.show) {
|
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
|
|
@@ -72,7 +75,7 @@ Examples:
|
|
|
72
75
|
`)
|
|
73
76
|
.option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
|
|
74
77
|
.option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
|
|
75
|
-
.option('--filter <expr>', 'Filter devices: "type=X", "name=X", "category=physical|ir", "room=X" (comma-separated key=value pairs)')
|
|
78
|
+
.option('--filter <expr>', 'Filter devices: "type=X", "name=X", "category=physical|ir", "room=X" (comma-separated key=value pairs)', stringArg('--filter'))
|
|
76
79
|
.action(async (options) => {
|
|
77
80
|
try {
|
|
78
81
|
const body = await fetchDeviceList();
|
|
@@ -195,8 +198,8 @@ Examples:
|
|
|
195
198
|
.command('status')
|
|
196
199
|
.description('Query the real-time status of a specific device')
|
|
197
200
|
.argument('[deviceId]', 'Device ID from "devices list" (or use --name or --ids)')
|
|
198
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
199
|
-
.option('--ids <list>', 'Comma-separated device IDs for batch status (incompatible with --name)')
|
|
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'))
|
|
200
203
|
.addHelpText('after', `
|
|
201
204
|
Status fields vary by device type. To discover them without a live call:
|
|
202
205
|
|
|
@@ -287,10 +290,10 @@ Examples:
|
|
|
287
290
|
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
288
291
|
.argument('[cmd]', 'Command name, e.g. turnOn, turnOff, setColor, setBrightness, setAll, startClean')
|
|
289
292
|
.argument('[parameter]', 'Command parameter. Omit for commands like turnOn/turnOff (defaults to "default"). Format depends on the command (see below).')
|
|
290
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
291
|
-
.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')
|
|
292
295
|
.option('--yes', 'Confirm a destructive command (Smart Lock unlock, Garage open, …). --dry-run is always allowed without --yes.')
|
|
293
|
-
.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'))
|
|
294
297
|
.addHelpText('after', `
|
|
295
298
|
────────────────────────────────────────────────────────────────────────
|
|
296
299
|
For the full list of commands a specific device supports — and their
|
|
@@ -361,6 +364,9 @@ Examples:
|
|
|
361
364
|
effectiveDeviceIdArg = deviceIdArg;
|
|
362
365
|
}
|
|
363
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
|
+
}
|
|
364
370
|
const validation = validateCommand(deviceId, cmd, parameter, options.type);
|
|
365
371
|
if (!validation.ok) {
|
|
366
372
|
const err = validation.error;
|
|
@@ -539,7 +545,7 @@ Examples:
|
|
|
539
545
|
.command('describe')
|
|
540
546
|
.description('Describe a device by ID: metadata + supported commands + status fields (1 API call)')
|
|
541
547
|
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
542
|
-
.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'))
|
|
543
549
|
.option('--live', 'Also fetch live status values and merge them into capabilities (costs 1 extra API call)')
|
|
544
550
|
.addHelpText('after', `
|
|
545
551
|
Makes a GET /v1.1/devices call to look up the device's type, then prints its
|
package/dist/commands/events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
3
|
+
import { intArg, stringArg } from '../utils/arg-parsers.js';
|
|
3
4
|
import { SwitchBotMqttClient } from '../mqtt/client.js';
|
|
4
5
|
import { fetchMqttCredential } from '../mqtt/credential.js';
|
|
5
6
|
import { tryLoadConfig } from '../config.js';
|
|
@@ -118,10 +119,10 @@ export function registerEventsCommand(program) {
|
|
|
118
119
|
events
|
|
119
120
|
.command('tail')
|
|
120
121
|
.description('Run a local HTTP receiver and print incoming webhook events as JSONL')
|
|
121
|
-
.option('--port <n>', `Local port to listen on (default ${DEFAULT_PORT})`, String(DEFAULT_PORT))
|
|
122
|
-
.option('--path <p>', `HTTP path to match (default "${DEFAULT_PATH}"; use "*" for all paths)`, DEFAULT_PATH)
|
|
123
|
-
.option('--filter <expr>', 'Filter events, e.g. "deviceId=ABC123" or "type=Bot" (comma-separated)')
|
|
124
|
-
.option('--max <n>', 'Stop after N matching events (default: run until Ctrl-C)')
|
|
122
|
+
.option('--port <n>', `Local port to listen on (default ${DEFAULT_PORT})`, intArg('--port', { min: 1, max: 65535 }), String(DEFAULT_PORT))
|
|
123
|
+
.option('--path <p>', `HTTP path to match (default "${DEFAULT_PATH}"; use "*" for all paths)`, stringArg('--path'), DEFAULT_PATH)
|
|
124
|
+
.option('--filter <expr>', 'Filter events, e.g. "deviceId=ABC123" or "type=Bot" (comma-separated)', stringArg('--filter'))
|
|
125
|
+
.option('--max <n>', 'Stop after N matching events (default: run until Ctrl-C)', intArg('--max', { min: 1 }))
|
|
125
126
|
.addHelpText('after', `
|
|
126
127
|
SwitchBot posts events to a single webhook URL configured via:
|
|
127
128
|
$ switchbot webhook setup https://<your-public-host>/<path>
|
|
@@ -199,20 +200,20 @@ Examples:
|
|
|
199
200
|
events
|
|
200
201
|
.command('mqtt-tail')
|
|
201
202
|
.description('Subscribe to SwitchBot MQTT shadow events and stream them as JSONL')
|
|
202
|
-
.option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)')
|
|
203
|
-
.option('--max <n>', 'Stop after N events (default: run until Ctrl-C)')
|
|
203
|
+
.option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)', stringArg('--topic'))
|
|
204
|
+
.option('--max <n>', 'Stop after N events (default: run until Ctrl-C)', intArg('--max', { min: 1 }))
|
|
204
205
|
.option('--sink <type>', 'Output sink: stdout (default), file, webhook, openclaw, telegram, homeassistant (repeatable)', (val, prev) => [...prev, val], [])
|
|
205
|
-
.option('--sink-file <path>', 'File path for file sink')
|
|
206
|
-
.option('--webhook-url <url>', 'Webhook URL for webhook sink')
|
|
207
|
-
.option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)')
|
|
208
|
-
.option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)')
|
|
209
|
-
.option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to')
|
|
210
|
-
.option('--telegram-token <token>', 'Telegram bot token (or env TELEGRAM_TOKEN)')
|
|
211
|
-
.option('--telegram-chat <id>', 'Telegram chat/channel ID to send messages to')
|
|
212
|
-
.option('--ha-url <url>', 'Home Assistant base URL (e.g. http://homeassistant.local:8123)')
|
|
213
|
-
.option('--ha-token <token>', 'HA long-lived access token (for REST event API)')
|
|
214
|
-
.option('--ha-webhook-id <id>', 'HA webhook ID (no auth; takes priority over --ha-token)')
|
|
215
|
-
.option('--ha-event-type <type>', 'HA event type for REST API (default: switchbot_event)')
|
|
206
|
+
.option('--sink-file <path>', 'File path for file sink', stringArg('--sink-file'))
|
|
207
|
+
.option('--webhook-url <url>', 'Webhook URL for webhook sink', stringArg('--webhook-url'))
|
|
208
|
+
.option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)', stringArg('--openclaw-url'))
|
|
209
|
+
.option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)', stringArg('--openclaw-token'))
|
|
210
|
+
.option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to', stringArg('--openclaw-model'))
|
|
211
|
+
.option('--telegram-token <token>', 'Telegram bot token (or env TELEGRAM_TOKEN)', stringArg('--telegram-token'))
|
|
212
|
+
.option('--telegram-chat <id>', 'Telegram chat/channel ID to send messages to', stringArg('--telegram-chat'))
|
|
213
|
+
.option('--ha-url <url>', 'Home Assistant base URL (e.g. http://homeassistant.local:8123)', stringArg('--ha-url'))
|
|
214
|
+
.option('--ha-token <token>', 'HA long-lived access token (for REST event API)', stringArg('--ha-token'))
|
|
215
|
+
.option('--ha-webhook-id <id>', 'HA webhook ID (no auth; takes priority over --ha-token)', stringArg('--ha-webhook-id'))
|
|
216
|
+
.option('--ha-event-type <type>', 'HA event type for REST API (default: switchbot_event)', stringArg('--ha-event-type'))
|
|
216
217
|
.addHelpText('after', `
|
|
217
218
|
Connects to the SwitchBot MQTT service using your existing credentials
|
|
218
219
|
(SWITCHBOT_TOKEN + SWITCHBOT_SECRET or ~/.switchbot/config.json).
|
|
@@ -343,10 +344,18 @@ Examples:
|
|
|
343
344
|
ac.abort();
|
|
344
345
|
}
|
|
345
346
|
});
|
|
347
|
+
let mqttFailed = false;
|
|
346
348
|
const unsubState = client.onStateChange((state) => {
|
|
347
349
|
if (!isJsonMode()) {
|
|
348
350
|
console.error(`[${new Date().toLocaleTimeString()}] MQTT state: ${state}`);
|
|
349
351
|
}
|
|
352
|
+
if (state === 'failed') {
|
|
353
|
+
mqttFailed = true;
|
|
354
|
+
if (!isJsonMode()) {
|
|
355
|
+
console.error('MQTT connection failed permanently (credential expired or reconnect exhausted) — exiting.');
|
|
356
|
+
}
|
|
357
|
+
ac.abort();
|
|
358
|
+
}
|
|
350
359
|
});
|
|
351
360
|
await client.connect();
|
|
352
361
|
client.subscribe(topic);
|
|
@@ -366,6 +375,10 @@ Examples:
|
|
|
366
375
|
process.once('SIGTERM', cleanup);
|
|
367
376
|
ac.signal.addEventListener('abort', cleanup, { once: true });
|
|
368
377
|
});
|
|
378
|
+
if (mqttFailed) {
|
|
379
|
+
// Surface as a runtime error so supervisors (pm2, systemd) can restart.
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
369
382
|
}
|
|
370
383
|
catch (error) {
|
|
371
384
|
handleError(error);
|
package/dist/commands/expand.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { intArg, stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { handleError, isJsonMode, printJson, UsageError } from '../utils/output.js';
|
|
2
3
|
import { getCachedDevice } from '../devices/cache.js';
|
|
3
4
|
import { executeCommand, isDestructiveCommand, getDestructiveReason } from '../lib/devices.js';
|
|
@@ -88,15 +89,15 @@ export function registerExpandCommand(devices) {
|
|
|
88
89
|
.description('Send a command with semantic flags instead of raw positional parameters')
|
|
89
90
|
.argument('[deviceId]', 'Target device ID from "devices list" (or use --name)')
|
|
90
91
|
.argument('[command]', 'Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2)')
|
|
91
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
92
|
-
.option('--temp <celsius>', 'AC setAll: temperature in Celsius (16-30)')
|
|
93
|
-
.option('--mode <mode>', 'AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary')
|
|
94
|
-
.option('--fan <speed>', 'AC setAll: fan speed auto|low|mid|high')
|
|
95
|
-
.option('--power <state>', 'AC setAll: on|off')
|
|
96
|
-
.option('--position <percent>', 'Curtain setPosition: 0-100 (0=open, 100=closed)')
|
|
97
|
-
.option('--direction <dir>', 'Blind Tilt setPosition: up|down')
|
|
98
|
-
.option('--angle <percent>', 'Blind Tilt setPosition: 0-100 (0=closed, 100=open)')
|
|
99
|
-
.option('--channel <n>', 'Relay Switch 2 setMode: channel 1 or 2')
|
|
92
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
93
|
+
.option('--temp <celsius>', 'AC setAll: temperature in Celsius (16-30)', intArg('--temp', { min: 16, max: 30 }))
|
|
94
|
+
.option('--mode <mode>', 'AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary', stringArg('--mode'))
|
|
95
|
+
.option('--fan <speed>', 'AC setAll: fan speed auto|low|mid|high', stringArg('--fan'))
|
|
96
|
+
.option('--power <state>', 'AC setAll: on|off', stringArg('--power'))
|
|
97
|
+
.option('--position <percent>', 'Curtain setPosition: 0-100 (0=open, 100=closed)', intArg('--position', { min: 0, max: 100 }))
|
|
98
|
+
.option('--direction <dir>', 'Blind Tilt setPosition: up|down', stringArg('--direction'))
|
|
99
|
+
.option('--angle <percent>', 'Blind Tilt setPosition: 0-100 (0=closed, 100=open)', intArg('--angle', { min: 0, max: 100 }))
|
|
100
|
+
.option('--channel <n>', 'Relay Switch 2 setMode: channel 1 or 2', intArg('--channel', { min: 1, max: 2 }))
|
|
100
101
|
.option('--yes', 'Confirm destructive commands')
|
|
101
102
|
.addHelpText('after', `
|
|
102
103
|
Translates semantic flags into the wire parameter format, then sends the command.
|
package/dist/commands/history.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
+
import { intArg, stringArg } from '../utils/arg-parsers.js';
|
|
3
4
|
import { printJson, isJsonMode, handleError } from '../utils/output.js';
|
|
4
5
|
import { readAudit } from '../utils/audit.js';
|
|
5
6
|
import { executeCommand } from '../lib/devices.js';
|
|
@@ -21,8 +22,8 @@ Examples:
|
|
|
21
22
|
history
|
|
22
23
|
.command('show')
|
|
23
24
|
.description('Print recent audit entries')
|
|
24
|
-
.option('--file <path>', `Path to the audit log (default ${DEFAULT_AUDIT})
|
|
25
|
-
.option('--limit <n>', 'Show only the last N entries')
|
|
25
|
+
.option('--file <path>', `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg('--file'))
|
|
26
|
+
.option('--limit <n>', 'Show only the last N entries', intArg('--limit', { min: 1 }))
|
|
26
27
|
.action((options) => {
|
|
27
28
|
const file = options.file ?? DEFAULT_AUDIT;
|
|
28
29
|
const entries = readAudit(file);
|
|
@@ -52,7 +53,7 @@ Examples:
|
|
|
52
53
|
.command('replay')
|
|
53
54
|
.description('Re-run a recorded command by its 1-indexed position')
|
|
54
55
|
.argument('<index>', 'Entry index (1 = oldest; as shown by "history show")')
|
|
55
|
-
.option('--file <path>', `Path to the audit log (default ${DEFAULT_AUDIT})
|
|
56
|
+
.option('--file <path>', `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg('--file'))
|
|
56
57
|
.addHelpText('after', `
|
|
57
58
|
Dry-run-honouring: pass --dry-run on the parent command to preview without
|
|
58
59
|
sending the actual call. Errors from the recorded entry are NOT replayed —
|
package/dist/commands/mcp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { intArg, stringArg } from '../utils/arg-parsers.js';
|
|
5
6
|
import { handleError, isJsonMode } from '../utils/output.js';
|
|
6
7
|
import { fetchDeviceList, fetchDeviceStatus, executeCommand, describeDevice, validateCommand, isDestructiveCommand, getDestructiveReason, searchCatalog, DeviceNotFoundError, toMcpDescribeShape, toMcpDeviceListShape, toMcpIrDeviceShape, } from '../lib/devices.js';
|
|
7
8
|
import { fetchScenes, executeScene } from '../lib/scenes.js';
|
|
@@ -489,11 +490,11 @@ Inspect locally:
|
|
|
489
490
|
mcp
|
|
490
491
|
.command('serve')
|
|
491
492
|
.description('Start the MCP server on stdio (default) or HTTP (--port)')
|
|
492
|
-
.option('--port <n>', 'Listen on HTTP instead of stdio (Streamable HTTP transport)')
|
|
493
|
-
.option('--bind <host>', 'IP address to bind (default 127.0.0.1; use 0.0.0.0 to accept external connections)', '127.0.0.1')
|
|
494
|
-
.option('--auth-token <token>', 'Bearer token for HTTP requests (required for --bind 0.0.0.0; falls back to SWITCHBOT_MCP_TOKEN env var)')
|
|
495
|
-
.option('--cors-origin <url>', 'Allowed CORS origin(s) for HTTP (repeatable)')
|
|
496
|
-
.option('--rate-limit <n>', 'Max requests per minute per profile (default 60)', '60')
|
|
493
|
+
.option('--port <n>', 'Listen on HTTP instead of stdio (Streamable HTTP transport)', intArg('--port', { min: 1, max: 65535 }))
|
|
494
|
+
.option('--bind <host>', 'IP address to bind (default 127.0.0.1; use 0.0.0.0 to accept external connections)', stringArg('--bind'), '127.0.0.1')
|
|
495
|
+
.option('--auth-token <token>', 'Bearer token for HTTP requests (required for --bind 0.0.0.0; falls back to SWITCHBOT_MCP_TOKEN env var)', stringArg('--auth-token'))
|
|
496
|
+
.option('--cors-origin <url>', 'Allowed CORS origin(s) for HTTP (repeatable)', stringArg('--cors-origin'))
|
|
497
|
+
.option('--rate-limit <n>', 'Max requests per minute per profile (default 60)', intArg('--rate-limit', { min: 1 }), '60')
|
|
497
498
|
.action(async (options) => {
|
|
498
499
|
try {
|
|
499
500
|
if (options.port) {
|
package/dist/commands/schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printJson } from '../utils/output.js';
|
|
2
3
|
import { getEffectiveCatalog } from '../devices/catalog.js';
|
|
3
4
|
function toSchemaEntry(e) {
|
|
@@ -24,15 +25,17 @@ function toSchemaCommand(c) {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
export function registerSchemaCommand(program) {
|
|
28
|
+
const ROLES = ['lighting', 'security', 'sensor', 'climate', 'media', 'cleaning', 'curtain', 'fan', 'power', 'hub', 'other'];
|
|
29
|
+
const CATEGORIES = ['physical', 'ir'];
|
|
27
30
|
const schema = program
|
|
28
31
|
.command('schema')
|
|
29
32
|
.description('Export the device catalog as structured JSON (for agent prompts / tooling)');
|
|
30
33
|
schema
|
|
31
34
|
.command('export')
|
|
32
35
|
.description('Print the full catalog as structured JSON (one object per type)')
|
|
33
|
-
.option('--type <type>', 'Restrict to a single device type (e.g. "Strip Light")')
|
|
34
|
-
.option('--role <role>', 'Restrict to a functional role: lighting, security, sensor, climate, media, cleaning, curtain, fan, power, hub, other')
|
|
35
|
-
.option('--category <cat>', 'Restrict to "physical" or "ir"')
|
|
36
|
+
.option('--type <type>', 'Restrict to a single device type (e.g. "Strip Light")', stringArg('--type'))
|
|
37
|
+
.option('--role <role>', 'Restrict to a functional role: lighting, security, sensor, climate, media, cleaning, curtain, fan, power, hub, other', enumArg('--role', ROLES))
|
|
38
|
+
.option('--category <cat>', 'Restrict to "physical" or "ir"', enumArg('--category', CATEGORIES))
|
|
36
39
|
.addHelpText('after', `
|
|
37
40
|
Output is always JSON (this command ignores --format). The output is a
|
|
38
41
|
catalog export — not a formal JSON Schema standard document — suitable for
|
package/dist/commands/watch.js
CHANGED
|
@@ -2,6 +2,7 @@ import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.
|
|
|
2
2
|
import { fetchDeviceStatus } from '../lib/devices.js';
|
|
3
3
|
import { getCachedDevice } from '../devices/cache.js';
|
|
4
4
|
import { parseDurationToMs, getFields } from '../utils/flags.js';
|
|
5
|
+
import { intArg, durationArg, stringArg } from '../utils/arg-parsers.js';
|
|
5
6
|
import { createClient } from '../api/client.js';
|
|
6
7
|
import { resolveDeviceId } from '../utils/name-resolver.js';
|
|
7
8
|
const DEFAULT_INTERVAL_MS = 30_000;
|
|
@@ -57,9 +58,9 @@ export function registerWatchCommand(devices) {
|
|
|
57
58
|
.command('watch')
|
|
58
59
|
.description('Poll device status on an interval and emit field-level changes (JSONL)')
|
|
59
60
|
.argument('[deviceId...]', 'One or more deviceIds to watch (or use --name for one device)')
|
|
60
|
-
.option('--name <query>', 'Resolve one device by fuzzy name (combined with any positional IDs)')
|
|
61
|
-
.option('--interval <dur>', `Polling interval: "30s", "1m", "500ms", ... (default 30s, min ${MIN_INTERVAL_MS / 1000}s)`, '30s')
|
|
62
|
-
.option('--max <n>', 'Stop after N ticks (default: run until Ctrl-C)')
|
|
61
|
+
.option('--name <query>', 'Resolve one device by fuzzy name (combined with any positional IDs)', stringArg('--name'))
|
|
62
|
+
.option('--interval <dur>', `Polling interval: "30s", "1m", "500ms", ... (default 30s, min ${MIN_INTERVAL_MS / 1000}s)`, durationArg('--interval'), '30s')
|
|
63
|
+
.option('--max <n>', 'Stop after N ticks (default: run until Ctrl-C)', intArg('--max', { min: 1 }))
|
|
63
64
|
.option('--include-unchanged', 'Emit a tick even when no field changed')
|
|
64
65
|
.addHelpText('after', `
|
|
65
66
|
Each poll emits one JSON line per deviceId with the shape:
|
package/dist/commands/webhook.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { createClient } from '../api/client.js';
|
|
2
3
|
import { printKeyValue, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
3
4
|
import chalk from 'chalk';
|
|
@@ -54,7 +55,7 @@ Example:
|
|
|
54
55
|
webhook
|
|
55
56
|
.command('query')
|
|
56
57
|
.description('Query webhook configuration')
|
|
57
|
-
.option('--details <url>', 'Query detailed configuration (enable/deviceList/timestamps) for a specific URL')
|
|
58
|
+
.option('--details <url>', 'Query detailed configuration (enable/deviceList/timestamps) for a specific URL', stringArg('--details'))
|
|
58
59
|
.addHelpText('after', `
|
|
59
60
|
Without --details, lists all configured webhook URLs.
|
|
60
61
|
With --details, prints enable/deviceList/createTime/lastUpdateTime for the given URL.
|
package/dist/config.js
CHANGED
|
@@ -99,7 +99,7 @@ export function showConfig() {
|
|
|
99
99
|
const envSecret = process.env.SWITCHBOT_SECRET;
|
|
100
100
|
if (envToken && envSecret) {
|
|
101
101
|
console.log('Credential source: environment variables');
|
|
102
|
-
console.log(`token : ${envToken}`);
|
|
102
|
+
console.log(`token : ${maskCredential(envToken)}`);
|
|
103
103
|
console.log(`secret: ${maskSecret(envSecret)}`);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
@@ -112,13 +112,18 @@ export function showConfig() {
|
|
|
112
112
|
const raw = fs.readFileSync(file, 'utf-8');
|
|
113
113
|
const cfg = JSON.parse(raw);
|
|
114
114
|
console.log(`Credential source: ${file}`);
|
|
115
|
-
console.log(`token : ${cfg.token}`);
|
|
115
|
+
console.log(`token : ${maskCredential(cfg.token)}`);
|
|
116
116
|
console.log(`secret: ${maskSecret(cfg.secret)}`);
|
|
117
117
|
}
|
|
118
118
|
catch {
|
|
119
119
|
console.error('Failed to read config file');
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
function maskCredential(token) {
|
|
123
|
+
if (token.length <= 8)
|
|
124
|
+
return '*'.repeat(Math.max(4, token.length));
|
|
125
|
+
return token.slice(0, 4) + '*'.repeat(token.length - 8) + token.slice(-4);
|
|
126
|
+
}
|
|
122
127
|
function maskSecret(secret) {
|
|
123
128
|
if (secret.length <= 4)
|
|
124
129
|
return '****';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command, CommanderError } from 'commander';
|
|
2
|
+
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import { intArg, stringArg, enumArg } from './utils/arg-parsers.js';
|
|
5
|
+
import { parseDurationToMs } from './utils/flags.js';
|
|
4
6
|
import { registerConfigCommand } from './commands/config.js';
|
|
5
7
|
import { registerDevicesCommand } from './commands/devices.js';
|
|
6
8
|
import { registerScenesCommand } from './commands/scenes.js';
|
|
@@ -19,26 +21,44 @@ import { registerCapabilitiesCommand } from './commands/capabilities.js';
|
|
|
19
21
|
const require = createRequire(import.meta.url);
|
|
20
22
|
const { version: pkgVersion } = require('../package.json');
|
|
21
23
|
const program = new Command();
|
|
24
|
+
// Top-level subcommand names. Used by stringArg to produce clearer errors when
|
|
25
|
+
// a value is omitted and the next argv token turns out to be a subcommand name.
|
|
26
|
+
const TOP_LEVEL_COMMANDS = [
|
|
27
|
+
'config', 'devices', 'scenes', 'webhook', 'completion', 'mcp',
|
|
28
|
+
'quota', 'catalog', 'cache', 'events', 'doctor', 'schema',
|
|
29
|
+
'history', 'plan', 'capabilities',
|
|
30
|
+
];
|
|
31
|
+
const cacheModeArg = (value) => {
|
|
32
|
+
if (value.startsWith('-')) {
|
|
33
|
+
throw new InvalidArgumentError(`--cache requires a mode value, got "${value}". ` +
|
|
34
|
+
`Valid: "off", "auto", or a duration like "5m", "1h". Use --cache=<mode> if needed.`);
|
|
35
|
+
}
|
|
36
|
+
if (value === 'off' || value === 'auto')
|
|
37
|
+
return value;
|
|
38
|
+
if (parseDurationToMs(value) !== null)
|
|
39
|
+
return value;
|
|
40
|
+
throw new InvalidArgumentError(`--cache must be "off", "auto", or a duration like "30s"/"5m"/"1h" (got "${value}")`);
|
|
41
|
+
};
|
|
22
42
|
program
|
|
23
43
|
.name('switchbot')
|
|
24
44
|
.description('Command-line tool for SwitchBot API v1.1')
|
|
25
45
|
.version(pkgVersion)
|
|
26
46
|
.option('--json', 'Output raw JSON response (disables tables; useful for pipes/scripts)')
|
|
27
|
-
.option('--format <type>', 'Output format: table (default), json, jsonl, tsv, yaml, id')
|
|
28
|
-
.option('--fields <csv>', 'Comma-separated list of columns to include (e.g. --fields=id,name,type)')
|
|
47
|
+
.option('--format <type>', 'Output format: table (default), json, jsonl, tsv, yaml, id', enumArg('--format', ['table', 'json', 'jsonl', 'tsv', 'yaml', 'id']))
|
|
48
|
+
.option('--fields <csv>', 'Comma-separated list of columns to include (e.g. --fields=id,name,type)', stringArg('--fields', { disallow: TOP_LEVEL_COMMANDS }))
|
|
29
49
|
.option('-v, --verbose', 'Log HTTP request/response details to stderr')
|
|
30
50
|
.option('--dry-run', 'Print mutating requests without sending them (GETs still execute)')
|
|
31
|
-
.option('--timeout <ms>', 'HTTP request timeout in milliseconds (default: 30000)')
|
|
32
|
-
.option('--retry-on-429 <n>', 'Max 429 retries before surfacing the error (default: 3)')
|
|
33
|
-
.option('--backoff <strategy>', 'Backoff strategy for retries: "linear" or "exponential" (default)')
|
|
51
|
+
.option('--timeout <ms>', 'HTTP request timeout in milliseconds (default: 30000)', intArg('--timeout', { min: 1 }))
|
|
52
|
+
.option('--retry-on-429 <n>', 'Max 429 retries before surfacing the error (default: 3)', intArg('--retry-on-429', { min: 0 }))
|
|
53
|
+
.option('--backoff <strategy>', 'Backoff strategy for retries: "linear" or "exponential" (default)', enumArg('--backoff', ['linear', 'exponential']))
|
|
34
54
|
.option('--no-retry', 'Disable 429 retries entirely (equivalent to --retry-on-429 0)')
|
|
35
55
|
.option('--no-quota', 'Disable the local ~/.switchbot/quota.json counter for this run')
|
|
36
|
-
.option('--cache <mode>', 'Cache mode: "off" | "auto" (default: list 1h, status off) | duration like 5m, 1h, 30s (enables both stores)')
|
|
56
|
+
.option('--cache <mode>', 'Cache mode: "off" | "auto" (default: list 1h, status off) | duration like 5m, 1h, 30s (enables both stores)', cacheModeArg)
|
|
37
57
|
.option('--no-cache', 'Disable cache reads (equivalent to --cache off)')
|
|
38
|
-
.option('--config <path>', 'Override credential file location (default: ~/.switchbot/config.json)')
|
|
39
|
-
.option('--profile <name>', 'Use a named profile: ~/.switchbot/profiles/<name>.json')
|
|
58
|
+
.option('--config <path>', 'Override credential file location (default: ~/.switchbot/config.json)', stringArg('--config', { disallow: TOP_LEVEL_COMMANDS }))
|
|
59
|
+
.option('--profile <name>', 'Use a named profile: ~/.switchbot/profiles/<name>.json', stringArg('--profile', { disallow: TOP_LEVEL_COMMANDS }))
|
|
40
60
|
.option('--audit-log', 'Append every mutating command to JSONL audit log (default path: ~/.switchbot/audit.log)')
|
|
41
|
-
.option('--audit-log-path <path>', 'Custom audit log file path; use together with --audit-log')
|
|
61
|
+
.option('--audit-log-path <path>', 'Custom audit log file path; use together with --audit-log', stringArg('--audit-log-path', { disallow: TOP_LEVEL_COMMANDS }))
|
|
42
62
|
.showHelpAfterError('(run with --help to see usage)')
|
|
43
63
|
.showSuggestionAfterError();
|
|
44
64
|
registerConfigCommand(program);
|
|
@@ -95,24 +115,33 @@ Discovery:
|
|
|
95
115
|
|
|
96
116
|
Docs: https://github.com/OpenWonderLabs/SwitchBotAPI
|
|
97
117
|
`);
|
|
98
|
-
// Map commander usage errors (unknown option, missing argument,
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
// Map commander usage errors (unknown option, missing argument, argParser
|
|
119
|
+
// InvalidArgumentError, etc.) to exit code 2. Commander's exitOverride is
|
|
120
|
+
// per-command: subcommand errors won't bubble to the root override, so walk
|
|
121
|
+
// every registered command and apply the same handler.
|
|
122
|
+
const usageExitHandler = (err) => {
|
|
101
123
|
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
|
|
102
124
|
process.exit(0);
|
|
103
125
|
}
|
|
104
|
-
// Everything else from commander (unknown option, missing argument,
|
|
105
|
-
// invalid choice, conflicting options, unknown command) is a usage error.
|
|
106
126
|
process.exit(2);
|
|
107
|
-
}
|
|
127
|
+
};
|
|
128
|
+
function applyExitOverride(cmd) {
|
|
129
|
+
cmd.exitOverride(usageExitHandler);
|
|
130
|
+
cmd.commands.forEach(applyExitOverride);
|
|
131
|
+
}
|
|
132
|
+
applyExitOverride(program);
|
|
108
133
|
try {
|
|
109
134
|
await program.parseAsync();
|
|
110
135
|
}
|
|
111
136
|
catch (err) {
|
|
112
|
-
//
|
|
113
|
-
//
|
|
137
|
+
// Subcommand-level CommanderErrors (e.g. InvalidArgumentError from an
|
|
138
|
+
// argParser on a subcommand option) don't always hit the root exitOverride.
|
|
139
|
+
// Mirror the root mapping so all usage errors surface as exit 2.
|
|
114
140
|
if (err instanceof CommanderError) {
|
|
115
|
-
|
|
141
|
+
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
process.exit(2);
|
|
116
145
|
}
|
|
117
146
|
throw err;
|
|
118
147
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import { parseDurationToMs } from './flags.js';
|
|
3
|
+
/**
|
|
4
|
+
* Commander argParser callbacks that fail fast when a required-value flag
|
|
5
|
+
* swallows the next token (another flag, a subcommand name, etc.) — the
|
|
6
|
+
* default Commander behavior is to take the next argv token verbatim.
|
|
7
|
+
*
|
|
8
|
+
* Use `--flag=<val>` form to pass values that legitimately start with `--`.
|
|
9
|
+
*/
|
|
10
|
+
export function intArg(flagName, opts) {
|
|
11
|
+
return (value) => {
|
|
12
|
+
if (value.startsWith('-')) {
|
|
13
|
+
throw new InvalidArgumentError(`${flagName} requires a numeric value, got "${value}". ` +
|
|
14
|
+
`Did you forget a value? Use ${flagName}=<n> if the value really starts with "-".`);
|
|
15
|
+
}
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
if (!Number.isInteger(n)) {
|
|
18
|
+
throw new InvalidArgumentError(`${flagName} must be an integer (got "${value}")`);
|
|
19
|
+
}
|
|
20
|
+
if (opts?.min !== undefined && n < opts.min) {
|
|
21
|
+
throw new InvalidArgumentError(`${flagName} must be >= ${opts.min} (got "${value}")`);
|
|
22
|
+
}
|
|
23
|
+
if (opts?.max !== undefined && n > opts.max) {
|
|
24
|
+
throw new InvalidArgumentError(`${flagName} must be <= ${opts.max} (got "${value}")`);
|
|
25
|
+
}
|
|
26
|
+
return String(n);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function durationArg(flagName) {
|
|
30
|
+
return (value) => {
|
|
31
|
+
if (value.startsWith('-')) {
|
|
32
|
+
throw new InvalidArgumentError(`${flagName} requires a duration value, got "${value}". ` +
|
|
33
|
+
`Use ${flagName}=<dur> if the value really starts with "-".`);
|
|
34
|
+
}
|
|
35
|
+
const ms = parseDurationToMs(value);
|
|
36
|
+
if (ms === null) {
|
|
37
|
+
throw new InvalidArgumentError(`${flagName} must look like "30s", "1m", "500ms", "1h" (got "${value}")`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function stringArg(flagName, opts) {
|
|
43
|
+
return (value) => {
|
|
44
|
+
if (value.startsWith('--')) {
|
|
45
|
+
throw new InvalidArgumentError(`${flagName} requires a value. "${value}" looks like another option — ` +
|
|
46
|
+
`did you forget the value? Use ${flagName}=<val> if your value really starts with "--".`);
|
|
47
|
+
}
|
|
48
|
+
if (opts?.disallow?.includes(value)) {
|
|
49
|
+
throw new InvalidArgumentError(`${flagName} requires a value but got "${value}", which is a subcommand name. ` +
|
|
50
|
+
`Did you forget the value? Use ${flagName}=<val> or put ${flagName} after the subcommand.`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function enumArg(flagName, allowed) {
|
|
56
|
+
return (value) => {
|
|
57
|
+
if (!allowed.includes(value)) {
|
|
58
|
+
throw new InvalidArgumentError(`${flagName} must be one of: ${allowed.join(', ')} (got "${value}")`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchbot/openapi-cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"switchbot",
|