@switchbot/openapi-cli 2.6.1 → 2.6.3

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 CHANGED
@@ -184,7 +184,10 @@ switchbot devices command --help
184
184
 
185
185
  Intercepts every non-GET request: the CLI prints the URL/body it would have
186
186
  sent, then exits `0` without contacting the API. `GET` requests (list, status,
187
- query) are still executed so you can preview the state involved.
187
+ query) are still executed so you can preview the state involved. Dry-run also
188
+ validates command names against the device catalog and rejects unknown commands
189
+ (exit 2) when the device type has a known catalog entry. Commands sent to
190
+ read-only sensors (e.g. Meter) are likewise rejected.
188
191
 
189
192
  ```bash
190
193
  switchbot devices command ABC123 turnOn --dry-run
@@ -305,7 +308,7 @@ Generic parameter shapes (which one applies is decided by the device — see the
305
308
 
306
309
  Parameters for `setAll` (Air Conditioner), `setPosition` (Curtain / Blind Tilt), `setMode` (Relay Switch), `setBrightness` (dimmable lights), and `setColor` (Color Bulb / Strip Light / Ceiling Light) are validated client-side before the request — malformed shapes, out-of-range values, and JSON for CSV fields all fail fast with exit 2. `setColor` accepts `R:G:B`, `R,G,B`, `#RRGGBB`, `#RGB`, and CSS named colors (`red`, `blue`, …); all normalize to `R:G:B` before hitting the API. Pass `--skip-param-validation` to bypass (escape hatch — prefer fixing the argument). Command names are also case-normalized against the catalog (e.g. `turnon` is auto-corrected to `turnOn` with a stderr warning); unknown names still exit 2 with the supported-commands list.
307
310
 
308
- Unknown deviceIds (not in the local cache) exit 2 by default so `--dry-run` is a reliable pre-flight gate. Run `switchbot devices list` first, or pass `--allow-unknown-device` for scripted pass-through.
311
+ Unknown deviceIds (not in the local cache) exit 2 by default so `--dry-run` is a reliable pre-flight gate. Unknown command names and commands on read-only sensors are also rejected during dry-run when the device type has a catalog entry. Run `switchbot devices list` first, or pass `--allow-unknown-device` for scripted pass-through.
309
312
 
310
313
  Negative numeric parameters (e.g. `setBrightness -1` for a probe) are passed through to the command validator instead of being swallowed by the flag parser as an unknown option.
311
314
 
@@ -717,7 +720,7 @@ switchbot cache clear --key status
717
720
 
718
721
  | Code | Meaning |
719
722
  | ---- | ------------------------------------------------------------------------------------------------------------------------- |
720
- | `0` | Success (including `--dry-run` intercept) |
723
+ | `0` | Success (including `--dry-run` intercept when validation passes) |
721
724
  | `1` | Runtime error — API error, network failure, missing credentials |
722
725
  | `2` | Usage error — bad flag, missing/invalid argument, unknown subcommand, unknown device type, invalid URL, conflicting flags |
723
726
 
@@ -305,6 +305,35 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
305
305
  effectiveParameter = pv.normalized;
306
306
  }
307
307
  }
308
+ // Bug #55: validateCommand is lenient by design (passes unknown device
309
+ // types, ambiguous catalog matches). For dry-run we need stricter
310
+ // checking — query the catalog directly and reject unknown commands
311
+ // when the catalog has a definitive match.
312
+ if (effectiveType !== 'customize') {
313
+ const catalogMatch = findCatalogEntry(cached.type);
314
+ if (catalogMatch && !Array.isArray(catalogMatch)) {
315
+ const builtinCmds = catalogMatch.commands.filter((c) => c.commandType !== 'customize');
316
+ if (builtinCmds.length > 0) {
317
+ const exactMatch = builtinCmds.find((c) => c.command === command);
318
+ const caseMatch = !exactMatch
319
+ ? builtinCmds.find((c) => c.command.toLowerCase() === command.toLowerCase())
320
+ : null;
321
+ if (!exactMatch && !caseMatch) {
322
+ const supported = [...new Set(builtinCmds.map((c) => c.command))].join(', ');
323
+ return mcpError('usage', 2, `"${command}" is not a supported command for ${cached.name} (${cached.type}).`, {
324
+ hint: `Supported commands: ${supported}`,
325
+ context: { validationKind: 'unknown-command', deviceType: cached.type, command },
326
+ });
327
+ }
328
+ }
329
+ else if (catalogMatch.readOnly) {
330
+ return mcpError('usage', 2, `${cached.name} (${cached.type}) is a read-only sensor — it has no control commands.`, {
331
+ hint: "Use 'get_device_status' to read this device instead.",
332
+ context: { validationKind: 'read-only-device', deviceType: cached.type, command },
333
+ });
334
+ }
335
+ }
336
+ }
308
337
  const wouldSend = {
309
338
  deviceId,
310
339
  command,
@@ -426,7 +455,22 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
426
455
  },
427
456
  }, async ({ sceneId, dryRun }) => {
428
457
  if (dryRun) {
429
- const wouldSend = { sceneId };
458
+ let scenes = [];
459
+ try {
460
+ scenes = await fetchScenes();
461
+ }
462
+ catch {
463
+ // network failure — degrade gracefully, skip validation
464
+ }
465
+ const found = scenes.find((s) => s.sceneId === sceneId);
466
+ if (scenes.length > 0 && !found) {
467
+ return mcpError('usage', 2, `Scene not found: ${sceneId}`, {
468
+ subKind: 'scene-not-found',
469
+ hint: "Check the sceneId with 'list_scenes' (IDs are case-sensitive).",
470
+ context: { sceneId, candidates: scenes.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })).slice(0, 5) },
471
+ });
472
+ }
473
+ const wouldSend = { sceneId, sceneName: found?.sceneName ?? null };
430
474
  const structured = { ok: true, dryRun: true, wouldSend };
431
475
  return {
432
476
  content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
@@ -1,6 +1,7 @@
1
1
  import { printJson, isJsonMode, handleError, StructuredUsageError } from '../utils/output.js';
2
2
  import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
3
3
  import { fetchScenes, executeScene } from '../lib/scenes.js';
4
+ import { isDryRun } from '../utils/flags.js';
4
5
  export function registerScenesCommand(program) {
5
6
  const scenes = program
6
7
  .command('scenes')
@@ -56,6 +57,16 @@ Example:
56
57
  candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
57
58
  });
58
59
  }
60
+ if (isDryRun()) {
61
+ const wouldSend = { method: 'POST', url: `/v1.1/scenes/${sceneId}/execute`, sceneId, sceneName: found.sceneName };
62
+ if (isJsonMode()) {
63
+ printJson({ dryRun: true, wouldSend });
64
+ }
65
+ else {
66
+ console.log(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
67
+ }
68
+ return;
69
+ }
59
70
  await executeScene(sceneId);
60
71
  if (isJsonMode()) {
61
72
  printJson({ ok: true, sceneId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
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",