@switchbot/openapi-cli 2.2.0 → 2.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.
@@ -129,8 +129,10 @@ export async function executeCommand(deviceId, cmd, parameter, commandType, clie
129
129
  /**
130
130
  * Validate a command against the locally-cached device → catalog mapping.
131
131
  * Returns `{ ok: true }` when validation passes or is skipped (unknown device,
132
- * custom IR button, etc.); returns `{ ok: false, error }` when the caller
133
- * should refuse the call.
132
+ * custom IR button, etc.). On a case-only mismatch the canonical command name
133
+ * is returned via `normalized` along with a `caseNormalizedFrom` field so the
134
+ * caller can emit a warning and continue with the canonical name.
135
+ * Returns `{ ok: false, error }` only when the caller should refuse the call.
134
136
  */
135
137
  export function validateCommand(deviceId, cmd, parameter, commandType) {
136
138
  if (commandType === 'customize')
@@ -144,24 +146,34 @@ export function validateCommand(deviceId, cmd, parameter, commandType) {
144
146
  const builtinCommands = match.commands.filter((c) => c.commandType !== 'customize');
145
147
  if (builtinCommands.length === 0)
146
148
  return { ok: true };
147
- const spec = builtinCommands.find((c) => c.command === cmd);
149
+ let spec = builtinCommands.find((c) => c.command === cmd);
150
+ let caseNormalizedFrom;
151
+ let normalizedCmd = cmd;
148
152
  if (!spec) {
149
153
  const unique = [...new Set(builtinCommands.map((c) => c.command))];
150
154
  const caseMatch = unique.find((c) => c.toLowerCase() === cmd.toLowerCase());
151
- const hint = caseMatch
152
- ? `Did you mean "${caseMatch}"? Supported commands: ${unique.join(', ')}`
153
- : `Supported commands: ${unique.join(', ')}`;
154
- return {
155
- ok: false,
156
- error: new CommandValidationError(`"${cmd}" is not a supported command for ${cached.name} (${cached.type}).`, 'unknown-command', hint),
157
- };
155
+ if (caseMatch) {
156
+ // Case-only mismatch: normalize and continue.
157
+ caseNormalizedFrom = cmd;
158
+ normalizedCmd = caseMatch;
159
+ spec = builtinCommands.find((c) => c.command === caseMatch);
160
+ }
161
+ else {
162
+ const hint = `Supported commands: ${unique.join(', ')}`;
163
+ return {
164
+ ok: false,
165
+ error: new CommandValidationError(`"${cmd}" is not a supported command for ${cached.name} (${cached.type}).`, 'unknown-command', hint),
166
+ };
167
+ }
158
168
  }
169
+ if (!spec)
170
+ return { ok: true, normalized: normalizedCmd, caseNormalizedFrom };
159
171
  const noParamExpected = spec.parameter === '—';
160
172
  const userProvidedParam = parameter !== undefined && parameter !== 'default';
161
173
  if (noParamExpected && userProvidedParam) {
162
174
  return {
163
175
  ok: false,
164
- error: new CommandValidationError(`"${cmd}" takes no parameter, but one was provided: "${parameter}".`, 'unexpected-parameter', `Try: switchbot devices command ${deviceId} ${cmd}`),
176
+ error: new CommandValidationError(`"${normalizedCmd}" takes no parameter, but one was provided: "${parameter}".`, 'unexpected-parameter', `Try: switchbot devices command ${deviceId} ${normalizedCmd}`),
165
177
  };
166
178
  }
167
179
  // Warn when a parameter is required but the user omitted it
@@ -170,12 +182,12 @@ export function validateCommand(deviceId, cmd, parameter, commandType) {
170
182
  const example = spec.exampleParams?.[0];
171
183
  return {
172
184
  ok: false,
173
- error: new CommandValidationError(`"${cmd}" requires a parameter (${spec.parameter}).`, 'missing-parameter', example
174
- ? `Example: switchbot devices command <deviceId> ${cmd} "${example}"`
185
+ error: new CommandValidationError(`"${normalizedCmd}" requires a parameter (${spec.parameter}).`, 'missing-parameter', example
186
+ ? `Example: switchbot devices command <deviceId> ${normalizedCmd} "${example}"`
175
187
  : `See: switchbot devices commands ${cached.type}`),
176
188
  };
177
189
  }
178
- return { ok: true };
190
+ return { ok: true, normalized: normalizedCmd, caseNormalizedFrom };
179
191
  }
180
192
  /**
181
193
  * Inspect catalog annotations to decide whether a command is destructive,
@@ -0,0 +1,65 @@
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
+ // Flag-like tokens (`--something`, `-x`) are rejected up-front.
13
+ // Pure negative integers (`-1`, `-42`) fall through to min/max so the
14
+ // error classifies as a range error rather than "requires a numeric value".
15
+ if (value.startsWith('-') && !/^-\d+$/.test(value)) {
16
+ throw new InvalidArgumentError(`${flagName} requires a numeric value, got "${value}". ` +
17
+ `Did you forget a value? Use ${flagName}=<n> if the value really starts with "-".`);
18
+ }
19
+ const n = Number(value);
20
+ if (!Number.isInteger(n)) {
21
+ throw new InvalidArgumentError(`${flagName} must be an integer (got "${value}")`);
22
+ }
23
+ if (opts?.min !== undefined && n < opts.min) {
24
+ throw new InvalidArgumentError(`${flagName} must be >= ${opts.min} (got "${value}")`);
25
+ }
26
+ if (opts?.max !== undefined && n > opts.max) {
27
+ throw new InvalidArgumentError(`${flagName} must be <= ${opts.max} (got "${value}")`);
28
+ }
29
+ return String(n);
30
+ };
31
+ }
32
+ export function durationArg(flagName) {
33
+ return (value) => {
34
+ if (value.startsWith('-')) {
35
+ throw new InvalidArgumentError(`${flagName} requires a duration value, got "${value}". ` +
36
+ `Use ${flagName}=<dur> if the value really starts with "-".`);
37
+ }
38
+ const ms = parseDurationToMs(value);
39
+ if (ms === null) {
40
+ throw new InvalidArgumentError(`${flagName} must look like "30s", "1m", "500ms", "1h" (got "${value}")`);
41
+ }
42
+ return value;
43
+ };
44
+ }
45
+ export function stringArg(flagName, opts) {
46
+ return (value) => {
47
+ if (value.startsWith('--')) {
48
+ throw new InvalidArgumentError(`${flagName} requires a value. "${value}" looks like another option — ` +
49
+ `did you forget the value? Use ${flagName}=<val> if your value really starts with "--".`);
50
+ }
51
+ if (opts?.disallow?.includes(value)) {
52
+ throw new InvalidArgumentError(`${flagName} requires a value but got "${value}", which is a subcommand name. ` +
53
+ `Did you forget the value? Use ${flagName}=<val> or put ${flagName} after the subcommand.`);
54
+ }
55
+ return value;
56
+ };
57
+ }
58
+ export function enumArg(flagName, allowed) {
59
+ return (value) => {
60
+ if (!allowed.includes(value)) {
61
+ throw new InvalidArgumentError(`${flagName} must be one of: ${allowed.join(', ')} (got "${value}")`);
62
+ }
63
+ return value;
64
+ };
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
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",