@switchbot/openapi-cli 3.6.0 โ†’ 3.6.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +217 -109
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ Under the hood every surface shares the same catalog, cache, and HMAC client โ€”
63
63
  - ๐ŸŽจ **Dual output modes** โ€” colorized tables by default; `--json` passthrough for `jq` and scripting
64
64
  - ๐Ÿ” **Secure credentials** โ€” HMAC-SHA256 signed requests; config file written with `0600`; env-var override for CI
65
65
  - ๐Ÿ” **Dry-run mode** โ€” preview every mutating request before it hits the API
66
- - ๐Ÿงช **Fully tested** โ€” 2370 Vitest tests, mocked axios, zero network in CI
66
+ - ๐Ÿงช **Fully tested** โ€” 2391 Vitest tests, mocked axios, zero network in CI
67
67
  - โšก **Shell completion** โ€” Bash / Zsh / Fish / PowerShell
68
68
 
69
69
  ## Requirements
@@ -633,7 +633,7 @@ switchbot doctor
633
633
  switchbot doctor --json
634
634
  ```
635
635
 
636
- Runs local checks (Node version, credentials, profiles, catalog, catalog-schema, cache, quota, clock, MQTT, policy, MCP, keychain, path, inventory, audit, daemon, health, notify-connectivity, release-notes) and exits 1 if any check fails. `warn` results exit 0. The MQTT check reports `ok` when REST credentials are configured (auto-provisioned on first use). The `notify-connectivity` check probes webhook URLs declared in `type: notify` actions. Use this to diagnose connectivity or config issues before running automation.
636
+ Runs local checks (Node version, credentials, profiles, catalog, catalog-schema, catalog-coverage, cache, quota, clock, MQTT, policy, MCP, keychain, path, inventory, audit, daemon, health, notify-connectivity, release-notes) and exits 1 if any check fails. `warn` results exit 0. The MQTT check reports `ok` when REST credentials are configured (auto-provisioned on first use). The `notify-connectivity` check probes webhook URLs declared in `type: notify` actions. Use this to diagnose connectivity or config issues before running automation.
637
637
 
638
638
  `--json` output includes `maturityScore` (0โ€“100) and `maturityLabel` (`production-ready` / `mostly-ready` / `needs-work` / `not-ready`) to give an at-a-glance readiness rating:
639
639
 
@@ -807,7 +807,7 @@ npm install
807
807
 
808
808
  npm run dev -- <args> # Run from TypeScript sources via tsx
809
809
  npm run build # Compile to dist/
810
- npm test # Run the Vitest suite (2370 tests)
810
+ npm test # Run the Vitest suite (2391 tests)
811
811
  npm run test:watch # Watch mode
812
812
  npm run test:coverage # Coverage report (v8, HTML + text)
813
813
  ```
package/dist/index.js CHANGED
@@ -7797,7 +7797,9 @@ var init_catalog = __esm({
7797
7797
  moveDetected: "Motion detected (boolean)",
7798
7798
  openState: "Contact sensor open/closed",
7799
7799
  status: "Device-specific status word",
7800
- lightLevel: "Ambient light level"
7800
+ lightLevel: "Ambient light level",
7801
+ detected: "Presence detected (boolean)",
7802
+ hubDeviceId: "Hub device identifier for hub-bound devices"
7801
7803
  };
7802
7804
  onOff = [
7803
7805
  { command: "turnOn", parameter: "\u2014", description: "Power on", idempotent: true },
@@ -7918,12 +7920,12 @@ var init_catalog = __esm({
7918
7920
  {
7919
7921
  type: "Relay Switch 2PM",
7920
7922
  category: "physical",
7921
- description: "Dual-channel relay switch with per-channel on/off/toggle and optional roller-shade mode.",
7923
+ description: 'Dual-channel relay switch with per-channel on/off/toggle and optional roller-shade mode. IMPORTANT: turnOn/turnOff/toggle require a channel parameter ("1" or "2") \u2014 omitting it is a usage error.',
7922
7924
  role: "power",
7923
7925
  commands: [
7924
- { command: "turnOn", parameter: "1 | 2 (channel)", description: "Turn on channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7925
- { command: "turnOff", parameter: "1 | 2 (channel)", description: "Turn off channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7926
- { command: "toggle", parameter: "1 | 2 (channel)", description: "Toggle channel 1 or 2", idempotent: false, exampleParams: ["1", "2"] },
7926
+ { command: "turnOn", parameter: "1 | 2 (channel \u2014 required)", description: "Turn on channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7927
+ { command: "turnOff", parameter: "1 | 2 (channel \u2014 required)", description: "Turn off channel 1 or 2", idempotent: true, exampleParams: ["1", "2"] },
7928
+ { command: "toggle", parameter: "1 | 2 (channel \u2014 required)", description: "Toggle channel 1 or 2", idempotent: false, exampleParams: ["1", "2"] },
7927
7929
  { command: "setMode", parameter: '"<channel>;<mode>" e.g. "1;0"', description: "Per-channel mode (see Relay Switch 1 modes)", idempotent: true, exampleParams: ["1;0", "2;3"] },
7928
7930
  { command: "setPosition", parameter: "0-100 (roller percentage)", description: "Roller-shade-pair mode only", idempotent: true, exampleParams: ["0", "50", "100"] }
7929
7931
  ],
@@ -7945,7 +7947,7 @@ var init_catalog = __esm({
7945
7947
  category: "physical",
7946
7948
  description: "Evaporative humidifier with multiple speed/auto/sleep/humidity modes and child lock.",
7947
7949
  role: "climate",
7948
- aliases: ["Evaporative Humidifier"],
7950
+ aliases: ["Evaporative Humidifier", "Evaporative Humidifier (Auto-refill)"],
7949
7951
  commands: [
7950
7952
  ...onOff,
7951
7953
  { command: "setMode", parameter: `'{"mode":1-8,"targetHumidify":0-100}'`, description: "mode: 1=lv4 2=lv3 3=lv2 4=lv1 5=humidity 6=sleep 7=auto 8=drying", idempotent: true, exampleParams: ['{"mode":7,"targetHumidify":50}'] },
@@ -8064,7 +8066,7 @@ var init_catalog = __esm({
8064
8066
  category: "physical",
8065
8067
  description: "Entry-level robot vacuum with start/stop/dock and four suction power levels.",
8066
8068
  role: "cleaning",
8067
- aliases: ["Robot Vacuum", "Robot Vacuum Cleaner S1 Plus", "K10+", "K10+ Pro"],
8069
+ aliases: ["Robot Vacuum", "S1", "S1 Plus", "Robot Vacuum Cleaner S1 Plus", "K10+", "K10+ Pro"],
8068
8070
  commands: [
8069
8071
  { command: "start", parameter: "\u2014", description: "Start cleaning", idempotent: true },
8070
8072
  { command: "stop", parameter: "\u2014", description: "Stop cleaning", idempotent: true },
@@ -8078,7 +8080,7 @@ var init_catalog = __esm({
8078
8080
  category: "physical",
8079
8081
  description: "Compact robot vacuum and mop combo with sweep/mop sessions, fan level, and water level.",
8080
8082
  role: "cleaning",
8081
- aliases: ["K10+ Pro Combo", "K20+ Pro", "K11+", "Robot Vacuum Cleaner K11+"],
8083
+ aliases: ["K10+ Pro Combo", "K20+ Pro", "Robot Vacuum Cleaner K20 Plus Pro", "K11+", "Robot Vacuum Cleaner K11+"],
8082
8084
  commands: [
8083
8085
  { command: "startClean", parameter: `'{"action":"sweep"|"mop","param":{"fanLevel":1-4,"times":1-2639999}}'`, description: "Begin a cleaning session", idempotent: false, exampleParams: ['{"action":"sweep","param":{"fanLevel":2,"times":1}}'] },
8084
8086
  { command: "pause", parameter: "\u2014", description: "Pause cleaning", idempotent: true },
@@ -8093,7 +8095,7 @@ var init_catalog = __esm({
8093
8095
  category: "physical",
8094
8096
  description: "Advanced floor cleaning robot with sweep/mop modes, self-wash dock, and humidifier refill.",
8095
8097
  role: "cleaning",
8096
- aliases: ["Robot Vacuum Cleaner S10", "Robot Vacuum Cleaner S20", "S20"],
8098
+ aliases: ["S10", "Robot Vacuum Cleaner S10", "Robot Vacuum Cleaner S20", "S20"],
8097
8099
  commands: [
8098
8100
  { command: "startClean", parameter: `'{"action":"sweep"|"sweep_mop","param":{"fanLevel":1-4,"waterLevel":1-2,"times":1-2639999}}'`, description: "Begin a cleaning session", idempotent: false, exampleParams: ['{"action":"sweep","param":{"fanLevel":2,"waterLevel":1,"times":1}}'] },
8099
8101
  { command: "pause", parameter: "\u2014", description: "Pause cleaning", idempotent: true },
@@ -8208,7 +8210,18 @@ var init_catalog = __esm({
8208
8210
  description: "Battery-powered temperature and humidity sensor; read-only, no control commands.",
8209
8211
  role: "sensor",
8210
8212
  readOnly: true,
8211
- aliases: ["Meter Plus", "MeterPro", "MeterPro(CO2)", "WoIOSensor"],
8213
+ aliases: [
8214
+ "MeterPlus",
8215
+ "Meter Plus",
8216
+ "Meter Plus (JP)",
8217
+ "Meter Plus (US)",
8218
+ "Outdoor Meter",
8219
+ "MeterPro",
8220
+ "Meter Pro",
8221
+ "MeterPro(CO2)",
8222
+ "Meter Pro (CO2 Monitor)",
8223
+ "WoIOSensor"
8224
+ ],
8212
8225
  commands: [],
8213
8226
  statusFields: ["temperature", "humidity", "battery", "version"]
8214
8227
  },
@@ -8221,6 +8234,16 @@ var init_catalog = __esm({
8221
8234
  commands: [],
8222
8235
  statusFields: ["battery", "version", "moveDetected", "brightness", "openState"]
8223
8236
  },
8237
+ {
8238
+ type: "Presence Sensor",
8239
+ category: "physical",
8240
+ description: "Presence detector that reports occupancy and ambient light through a paired hub; read-only.",
8241
+ role: "sensor",
8242
+ readOnly: true,
8243
+ aliases: ["Human Presence Sensor", "Prensence Sensor"],
8244
+ commands: [],
8245
+ statusFields: ["version", "battery", "lightLevel", "detected", "hubDeviceId"]
8246
+ },
8224
8247
  {
8225
8248
  type: "Contact Sensor",
8226
8249
  category: "physical",
@@ -8256,7 +8279,7 @@ var init_catalog = __esm({
8256
8279
  description: "IR hub that bridges BLE devices to the cloud and learns IR remotes; no direct control commands.",
8257
8280
  role: "hub",
8258
8281
  readOnly: true,
8259
- aliases: ["Hub Mini2"],
8282
+ aliases: ["Hub", "Hub Plus", "Hub Mini2"],
8260
8283
  commands: [],
8261
8284
  statusFields: ["version"]
8262
8285
  },
@@ -8305,6 +8328,41 @@ var init_catalog = __esm({
8305
8328
  commands: [],
8306
8329
  statusFields: ["battery", "version"]
8307
8330
  },
8331
+ {
8332
+ type: "Indoor Cam",
8333
+ category: "physical",
8334
+ description: "Indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
8335
+ role: "security",
8336
+ readOnly: true,
8337
+ commands: []
8338
+ },
8339
+ {
8340
+ type: "Pan/Tilt Cam",
8341
+ category: "physical",
8342
+ description: "Pan/tilt indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
8343
+ role: "security",
8344
+ readOnly: true,
8345
+ aliases: ["Pan/Tilt Cam 2K"],
8346
+ commands: []
8347
+ },
8348
+ {
8349
+ type: "Pan/Tilt Cam Plus 3K",
8350
+ category: "physical",
8351
+ description: "Pan/tilt indoor security camera; listed by the cloud API but exposes no cloud status or control commands.",
8352
+ role: "security",
8353
+ readOnly: true,
8354
+ aliases: ["Pan/Tilt Cam Plus 2K", "Pan/Tilt Cam Plus", "Pan Tilt Cam Plus 3K"],
8355
+ commands: []
8356
+ },
8357
+ {
8358
+ type: "Remote",
8359
+ category: "physical",
8360
+ description: "Bluetooth wireless button remote paired to a hub or device; listed by the cloud API but exposes no cloud status or control commands.",
8361
+ role: "other",
8362
+ readOnly: true,
8363
+ aliases: ["SwitchBot Remote", "Remote Button", "Wireless Remote"],
8364
+ commands: []
8365
+ },
8308
8366
  // ---------- Virtual IR remotes ----------
8309
8367
  {
8310
8368
  type: "Air Conditioner",
@@ -32221,15 +32279,15 @@ function validateSetBrightness(raw, deviceType) {
32221
32279
  error: `setBrightness requires an integer ${min}-${max} (percent). Example: "50".`
32222
32280
  };
32223
32281
  }
32224
- const trimmed = stripQuotes(raw.trim());
32225
- if (!/^-?\d+$/.test(trimmed)) {
32282
+ const parsed = parseStrictInt(raw);
32283
+ if (!parsed.ok) {
32226
32284
  return {
32227
32285
  ok: false,
32228
32286
  error: `setBrightness must be an integer ${min}-${max}, got ${JSON.stringify(raw)}. ${hintBrightnessRetry(min, max)}`
32229
32287
  };
32230
32288
  }
32231
- const n = Number(trimmed);
32232
- if (!Number.isInteger(n) || n < min || n > max) {
32289
+ const n = parsed.value;
32290
+ if (n < min || n > max) {
32233
32291
  return {
32234
32292
  ok: false,
32235
32293
  error: `setBrightness must be an integer ${min}-${max}, got "${raw}". ${hintBrightnessRetry(min, max)}`
@@ -32321,15 +32379,15 @@ function validateSetColorTemperature(raw) {
32321
32379
  error: `setColorTemperature requires an integer Kelvin value 2700-6500. Example: "4000".`
32322
32380
  };
32323
32381
  }
32324
- const trimmed = stripQuotes(raw.trim());
32325
- if (!/^-?\d+$/.test(trimmed)) {
32382
+ const parsed = parseStrictInt(raw);
32383
+ if (!parsed.ok) {
32326
32384
  return {
32327
32385
  ok: false,
32328
32386
  error: `setColorTemperature must be an integer 2700-6500, got ${JSON.stringify(raw)}.`
32329
32387
  };
32330
32388
  }
32331
- const n = Number(trimmed);
32332
- if (!Number.isInteger(n) || n < 2700 || n > 6500) {
32389
+ const n = parsed.value;
32390
+ if (n < 2700 || n > 6500) {
32333
32391
  return {
32334
32392
  ok: false,
32335
32393
  error: `setColorTemperature must be an integer 2700-6500, got "${raw}".`
@@ -32359,27 +32417,30 @@ function validateAcSetAll(raw) {
32359
32417
  };
32360
32418
  }
32361
32419
  const [tempStr, modeStr, fanStr, powerStr] = parts.map((s2) => s2.trim());
32362
- const temp = Number(tempStr);
32363
- if (!Number.isInteger(temp) || temp < 16 || temp > 30) {
32420
+ const tempP = parseStrictInt(tempStr);
32421
+ if (!tempP.ok || tempP.value < 16 || tempP.value > 30) {
32364
32422
  return {
32365
32423
  ok: false,
32366
32424
  error: `setAll field 1 (temp) must be an integer 16-30, got "${tempStr}". Example: "26,2,2,on".`
32367
32425
  };
32368
32426
  }
32369
- const mode = Number(modeStr);
32370
- if (!Number.isInteger(mode) || mode < 1 || mode > 5) {
32427
+ const temp = tempP.value;
32428
+ const modeP = parseStrictInt(modeStr);
32429
+ if (!modeP.ok || modeP.value < 1 || modeP.value > 5) {
32371
32430
  return {
32372
32431
  ok: false,
32373
32432
  error: `setAll field 2 (mode) must be 1-5 (1=auto 2=cool 3=dry 4=fan 5=heat), got "${modeStr}". Example: "26,2,2,on".`
32374
32433
  };
32375
32434
  }
32376
- const fan = Number(fanStr);
32377
- if (!Number.isInteger(fan) || fan < 1 || fan > 4) {
32435
+ const mode = modeP.value;
32436
+ const fanP = parseStrictInt(fanStr);
32437
+ if (!fanP.ok || fanP.value < 1 || fanP.value > 4) {
32378
32438
  return {
32379
32439
  ok: false,
32380
32440
  error: `setAll field 3 (fan) must be 1-4 (1=auto 2=low 3=mid 4=high), got "${fanStr}". Example: "26,2,2,on".`
32381
32441
  };
32382
32442
  }
32443
+ const fan = fanP.value;
32383
32444
  const power = powerStr.toLowerCase();
32384
32445
  if (power !== "on" && power !== "off") {
32385
32446
  return {
@@ -32398,14 +32459,14 @@ function validateCurtainSetPosition(raw) {
32398
32459
  }
32399
32460
  const stripped = stripQuotes(raw.trim());
32400
32461
  if (!stripped.includes(",")) {
32401
- const pos2 = Number(stripped);
32402
- if (!Number.isInteger(pos2) || pos2 < 0 || pos2 > 100) {
32462
+ const posP2 = parseStrictInt(stripped);
32463
+ if (!posP2.ok || posP2.value < 0 || posP2.value > 100) {
32403
32464
  return {
32404
32465
  ok: false,
32405
32466
  error: `setPosition must be an integer 0-100, got "${raw}". Example: "50".`
32406
32467
  };
32407
32468
  }
32408
- return { ok: true, normalized: String(pos2) };
32469
+ return { ok: true, normalized: String(posP2.value) };
32409
32470
  }
32410
32471
  const parts = stripped.split(",").map((s2) => s2.trim());
32411
32472
  if (parts.length !== 3) {
@@ -32415,13 +32476,14 @@ function validateCurtainSetPosition(raw) {
32415
32476
  };
32416
32477
  }
32417
32478
  const [idxStr, modeStr, posStr] = parts;
32418
- const idx = Number(idxStr);
32419
- if (!Number.isInteger(idx) || idx < 0) {
32479
+ const idxP = parseStrictInt(idxStr);
32480
+ if (!idxP.ok || idxP.value < 0) {
32420
32481
  return {
32421
32482
  ok: false,
32422
32483
  error: `setPosition field 1 (index) must be a non-negative integer, got "${idxStr}".`
32423
32484
  };
32424
32485
  }
32486
+ const idx = idxP.value;
32425
32487
  const modeLower = modeStr.toLowerCase();
32426
32488
  if (!["ff", "0", "1"].includes(modeLower)) {
32427
32489
  return {
@@ -32429,13 +32491,14 @@ function validateCurtainSetPosition(raw) {
32429
32491
  error: `setPosition field 2 (mode) must be "ff", "0", or "1", got "${modeStr}". (ff=default, 0=performance, 1=silent)`
32430
32492
  };
32431
32493
  }
32432
- const pos = Number(posStr);
32433
- if (!Number.isInteger(pos) || pos < 0 || pos > 100) {
32494
+ const posP = parseStrictInt(posStr);
32495
+ if (!posP.ok || posP.value < 0 || posP.value > 100) {
32434
32496
  return {
32435
32497
  ok: false,
32436
32498
  error: `setPosition field 3 (position) must be an integer 0-100, got "${posStr}".`
32437
32499
  };
32438
32500
  }
32501
+ const pos = posP.value;
32439
32502
  return { ok: true, normalized: `${idx},${modeLower},${pos}` };
32440
32503
  }
32441
32504
  function validateBlindTiltSetPosition(raw) {
@@ -32460,13 +32523,14 @@ function validateBlindTiltSetPosition(raw) {
32460
32523
  error: `Blind Tilt setPosition direction must be "up" or "down", got "${parts[0]}".`
32461
32524
  };
32462
32525
  }
32463
- const angle = Number(parts[1]);
32464
- if (!Number.isInteger(angle) || angle < 0 || angle > 100) {
32526
+ const angleP = parseStrictInt(parts[1]);
32527
+ if (!angleP.ok || angleP.value < 0 || angleP.value > 100) {
32465
32528
  return {
32466
32529
  ok: false,
32467
32530
  error: `Blind Tilt setPosition angle must be an integer 0-100, got "${parts[1]}".`
32468
32531
  };
32469
32532
  }
32533
+ const angle = angleP.value;
32470
32534
  if (angle % 2 !== 0) {
32471
32535
  return {
32472
32536
  ok: false,
@@ -32490,21 +32554,22 @@ function validateRelay2PmSetMode(raw) {
32490
32554
  error: `Relay Switch setMode expects "<channel>;<mode>", got ${JSON.stringify(raw)}. Example: "1;1".`
32491
32555
  };
32492
32556
  }
32493
- const ch = Number(parts[0]);
32494
- if (ch !== 1 && ch !== 2) {
32557
+ const chP = parseStrictInt(parts[0]);
32558
+ if (!chP.ok || chP.value !== 1 && chP.value !== 2) {
32495
32559
  return {
32496
32560
  ok: false,
32497
32561
  error: `Relay Switch setMode channel must be 1 or 2, got "${parts[0]}".`
32498
32562
  };
32499
32563
  }
32500
- const mode = Number(parts[1]);
32501
- if (!Number.isInteger(mode) || mode < 0 || mode > 3) {
32564
+ const ch = chP.value;
32565
+ const modeP = parseStrictInt(parts[1]);
32566
+ if (!modeP.ok || modeP.value < 0 || modeP.value > 3) {
32502
32567
  return {
32503
32568
  ok: false,
32504
32569
  error: `Relay Switch setMode mode must be 0-3 (0=toggle 1=edge 2=detached 3=momentary), got "${parts[1]}".`
32505
32570
  };
32506
32571
  }
32507
- return { ok: true, normalized: `${ch};${mode}` };
32572
+ return { ok: true, normalized: `${ch};${modeP.value}` };
32508
32573
  }
32509
32574
  function validateRelayChannel(raw) {
32510
32575
  if (raw === void 0 || raw === "" || raw === "default") {
@@ -32528,6 +32593,11 @@ function stripQuotes(s2) {
32528
32593
  }
32529
32594
  return s2;
32530
32595
  }
32596
+ function parseStrictInt(raw) {
32597
+ const cleaned = stripQuotes(raw);
32598
+ if (!/^[+-]?(?:0|[1-9]\d*)$/.test(cleaned)) return { ok: false };
32599
+ return { ok: true, value: Number(cleaned) };
32600
+ }
32531
32601
  function validateIntRange(raw, command, min, max, label) {
32532
32602
  if (raw === void 0 || raw === "" || raw === "default") {
32533
32603
  return {
@@ -32535,14 +32605,14 @@ function validateIntRange(raw, command, min, max, label) {
32535
32605
  error: `${command} requires an integer ${min}-${max} (${label}). Example: "${Math.round((min + max) / 2)}".`
32536
32606
  };
32537
32607
  }
32538
- const trimmed = stripQuotes(raw.trim());
32539
- if (!/^-?\d+$/.test(trimmed)) {
32608
+ const parsed = parseStrictInt(raw);
32609
+ if (!parsed.ok) {
32540
32610
  return {
32541
32611
  ok: false,
32542
32612
  error: `${command} must be an integer ${min}-${max} (${label}), got ${JSON.stringify(raw)}.`
32543
32613
  };
32544
32614
  }
32545
- const n = Number(trimmed);
32615
+ const n = parsed.value;
32546
32616
  if (n < min || n > max) {
32547
32617
  return {
32548
32618
  ok: false,
@@ -33205,9 +33275,9 @@ Examples:
33205
33275
  printJson(result);
33206
33276
  } else {
33207
33277
  if (dryRunned.length > 0) {
33208
- console.log(`
33278
+ console.error(`
33209
33279
  Planned (dry-run): ${dryRunned.length} device(s)`);
33210
- for (const d of dryRunned) console.log(` - ${d.deviceId}`);
33280
+ for (const d of dryRunned) console.error(` - ${d.deviceId}`);
33211
33281
  }
33212
33282
  if (preSkipped.length > 0) {
33213
33283
  console.log(`
@@ -33842,7 +33912,7 @@ Examples:
33842
33912
  if (isJsonMode()) {
33843
33913
  printJson({ ok: true, dryRun: true, command, deviceId });
33844
33914
  } else {
33845
- console.log(`\u25E6 dry-run: ${command} would be sent to ${deviceId}`);
33915
+ console.error(`\u25E6 dry-run: ${command} would be sent to ${deviceId}`);
33846
33916
  }
33847
33917
  return;
33848
33918
  }
@@ -34204,7 +34274,7 @@ Total: ${totalLabel}`);
34204
34274
  handleError(error48);
34205
34275
  }
34206
34276
  });
34207
- devices.command("status").description("Query the real-time status of a specific device").argument("[deviceId]", 'Device ID from "devices list" (or use --name or --ids)').option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: fuzzy)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--ids <list>", "Comma-separated device IDs for batch status (incompatible with --name)", stringArg("--ids")).addHelpText("after", `
34277
+ devices.command("status").description("Query the real-time status of a specific device").argument("[deviceId...]", 'Device ID(s) from "devices list" (or use --name or --ids)').option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: fuzzy)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--ids <list>", "Comma-separated device IDs for batch status (incompatible with --name)", stringArg("--ids")).addHelpText("after", `
34208
34278
  Status fields vary by device type. To discover them without a live call:
34209
34279
 
34210
34280
  switchbot devices commands <type> (prints the "Status fields" section)
@@ -34214,6 +34284,7 @@ all field names returned by your specific device, then narrow with --fields.
34214
34284
 
34215
34285
  Examples:
34216
34286
  $ switchbot devices status ABC123DEF456
34287
+ $ switchbot devices status ABC123 DEF456 GHI789
34217
34288
  $ switchbot devices status --name "Living Room AC"
34218
34289
  $ switchbot devices status ABC123DEF456 --json
34219
34290
  $ switchbot devices status ABC123DEF456 --format yaml
@@ -34221,12 +34292,13 @@ Examples:
34221
34292
  $ switchbot devices status ABC123DEF456 --json | jq '.data.battery'
34222
34293
  $ switchbot devices status --ids ABC123,DEF456,GHI789
34223
34294
  $ switchbot devices status --ids ABC123,DEF456 --fields power,battery
34224
- `).action(async (deviceIdArg, options) => {
34295
+ `).action(async (deviceIdArgs, options) => {
34225
34296
  try {
34226
- if (options.ids) {
34297
+ const batchIds = options.ids ? options.ids.split(",").map((s2) => s2.trim()).filter(Boolean) : deviceIdArgs.length > 1 ? deviceIdArgs : void 0;
34298
+ if (batchIds) {
34227
34299
  if (options.name) throw new UsageError("--ids and --name cannot be used together.");
34228
- const ids = options.ids.split(",").map((s2) => s2.trim()).filter(Boolean);
34229
- if (ids.length === 0) throw new UsageError("--ids requires at least one device ID.");
34300
+ if (batchIds.length === 0) throw new UsageError("--ids requires at least one device ID.");
34301
+ const ids = batchIds;
34230
34302
  const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
34231
34303
  const fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString();
34232
34304
  const batch = results.map(
@@ -34258,7 +34330,7 @@ Examples:
34258
34330
  }
34259
34331
  return;
34260
34332
  }
34261
- const deviceId = resolveDeviceId(deviceIdArg, options.name, {
34333
+ const deviceId = resolveDeviceId(deviceIdArgs[0], options.name, {
34262
34334
  strategy: options.nameStrategy ?? "fuzzy",
34263
34335
  type: options.nameType,
34264
34336
  category: options.nameCategory,
@@ -34536,7 +34608,7 @@ ${extra}` : extra;
34536
34608
  if (isJsonMode()) {
34537
34609
  printJson({ dryRun: true, wouldSend });
34538
34610
  } else {
34539
- console.log(`\u25E6 dry-run intercepted for ${_cmd} on ${_deviceId}; see stderr preview for the HTTP request.`);
34611
+ console.error(`\u25E6 dry-run intercepted for ${_cmd} on ${_deviceId}; see stderr preview for the HTTP request.`);
34540
34612
  }
34541
34613
  return;
34542
34614
  }
@@ -34787,12 +34859,15 @@ function renderCatalogEntry(entry) {
34787
34859
  console.log(`Type: ${entry.type}`);
34788
34860
  console.log(`Category: ${entry.category === "ir" ? "IR remote" : "Physical device"}`);
34789
34861
  if (entry.role) console.log(`Role: ${entry.role}`);
34790
- if (entry.readOnly) console.log(`ReadOnly: yes (status-only device, no control commands)`);
34862
+ const hasStatusFields = (entry.statusFields?.length ?? 0) > 0;
34863
+ if (entry.readOnly) {
34864
+ console.log(hasStatusFields ? `ReadOnly: yes (status-only device, no control commands)` : `ReadOnly: yes (no cloud control commands cataloged)`);
34865
+ }
34791
34866
  if (entry.aliases && entry.aliases.length > 0) {
34792
34867
  console.log(`Aliases: ${entry.aliases.join(", ")}`);
34793
34868
  }
34794
34869
  if (entry.commands.length === 0) {
34795
- console.log("\nCommands: (none \u2014 status-only device)");
34870
+ console.log(hasStatusFields ? "\nCommands: (none \u2014 status-only device)" : "\nCommands: (none \u2014 no cloud control commands cataloged)");
34796
34871
  } else {
34797
34872
  console.log("\nCommands:");
34798
34873
  const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
@@ -34897,7 +34972,7 @@ Example:
34897
34972
  if (isJsonMode()) {
34898
34973
  printJson({ dryRun: true, wouldSend });
34899
34974
  } else {
34900
- console.log(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
34975
+ console.error(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
34901
34976
  }
34902
34977
  return;
34903
34978
  }
@@ -35055,7 +35130,7 @@ Example:
35055
35130
  console.log(`idempotent: unknown (scene steps not exposed by API)`);
35056
35131
  console.log(`toExecute: ${explanation.toExecute}`);
35057
35132
  if (explanation.dryRun) {
35058
- console.log(`dryRun: true (pass --dry-run to execute would be a no-op)`);
35133
+ console.error(`dryRun: true (pass --dry-run to execute would be a no-op)`);
35059
35134
  }
35060
35135
  console.log(`note: ${explanation.note}`);
35061
35136
  } catch (error48) {
@@ -50545,7 +50620,7 @@ async function executePlanSteps(plan, planId, options) {
50545
50620
  if (err instanceof Error && err.name === "DryRunSignal") {
50546
50621
  out.results.push({ step: idx, type: "command", deviceId: resolvedDeviceId, command: step.command, status: "dry-run" });
50547
50622
  out.summary.dryRun++;
50548
- if (!isJsonMode()) console.log(` ${idx}. \u25E6 dry-run ${step.command} on ${resolvedDeviceId}`);
50623
+ if (!isJsonMode()) console.error(` ${idx}. \u25E6 dry-run ${step.command} on ${resolvedDeviceId}`);
50549
50624
  continue;
50550
50625
  }
50551
50626
  const msg = err instanceof Error ? err.message : String(err);
@@ -53515,21 +53590,21 @@ Example Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
53515
53590
  Inspect locally:
53516
53591
  $ npx @modelcontextprotocol/inspector switchbot mcp serve
53517
53592
  `);
53518
- mcp.command("tools").description("Print the registered MCP tools in human or JSON form").option("--tools <profile>", "Tool profile: default, readonly, all (default: all)", stringArg("--tools"), "all").action((opts) => {
53593
+ mcp.command("tools").description("Print the registered MCP tools in human or JSON form").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24). Lists all when omitted', stringArg("--tools"), "all").action((opts) => {
53519
53594
  try {
53520
53595
  printMcpToolDirectory(resolveToolProfile(opts.tools));
53521
53596
  } catch (e) {
53522
53597
  handleError(e);
53523
53598
  }
53524
53599
  });
53525
- mcp.command("list-tools").description("Alias of `mcp tools`").option("--tools <profile>", "Tool profile: default, readonly, all (default: all)", stringArg("--tools"), "all").action((opts) => {
53600
+ mcp.command("list-tools").description("Alias of `mcp tools`").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24). Lists all when omitted', stringArg("--tools"), "all").action((opts) => {
53526
53601
  try {
53527
53602
  printMcpToolDirectory(resolveToolProfile(opts.tools));
53528
53603
  } catch (e) {
53529
53604
  handleError(e);
53530
53605
  }
53531
53606
  });
53532
- mcp.command("serve").description("Start the MCP server on stdio (default) or HTTP (--port)").option("--port <n>", "Listen on HTTP instead of stdio (Streamable HTTP transport)", intArg("--port", { min: 1, max: 65535 })).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").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")).option("--cors-origin <url>", "Allowed CORS origin(s) for HTTP (repeatable)", stringArg("--cors-origin")).option("--rate-limit <n>", "Max requests per minute per profile (default 60)", intArg("--rate-limit", { min: 1 }), "60").option("--tools <profile>", "Tool profile: default, readonly, all (default: default)", stringArg("--tools"), "default").addHelpText("after", `
53607
+ mcp.command("serve").description("Start the MCP server on stdio (default) or HTTP (--port)").option("--port <n>", "Listen on HTTP instead of stdio (Streamable HTTP transport)", intArg("--port", { min: 1, max: 65535 })).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").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")).option("--cors-origin <url>", "Allowed CORS origin(s) for HTTP (repeatable)", stringArg("--cors-origin")).option("--rate-limit <n>", "Max requests per minute per profile (default 60)", intArg("--rate-limit", { min: 1 }), "60").option("--tools <profile>", 'Tool profile: "default" (13 tools), "readonly" (10), or "all" (24)', stringArg("--tools"), "default").addHelpText("after", `
53533
53608
  Examples:
53534
53609
  $ switchbot mcp serve
53535
53610
  $ switchbot mcp serve --tools all
@@ -53977,6 +54052,7 @@ Total: ${entries.length} device type(s) (source: ${source})`);
53977
54052
  });
53978
54053
  catalog.command("search").description("Fuzzy search the effective catalog by type name, alias, role, or command name").argument("<keyword>", "Substring to match (case-insensitive) against type, alias, role, or command").option("--strict", "Only return entries whose type name matches (skip alias/role/command fallbacks)").action((keyword, options) => {
53979
54054
  try {
54055
+ if (!keyword.trim()) throw new UsageError("catalog search requires a non-empty keyword.");
53980
54056
  const q = keyword.toLowerCase();
53981
54057
  const entries = getEffectiveCatalog();
53982
54058
  const strict = options.strict === true;
@@ -55541,6 +55617,25 @@ function checkCatalog() {
55541
55617
  detail: `${catalog.length} types loaded${missingRole > 0 ? `, ${missingRole} missing role` : ""}`
55542
55618
  };
55543
55619
  }
55620
+ function checkCatalogCoverage() {
55621
+ const cache2 = loadCache();
55622
+ if (!cache2) {
55623
+ return { name: "catalog-coverage", status: "ok", detail: 'no device cache \u2014 run "switchbot devices list" first' };
55624
+ }
55625
+ const catalog = getEffectiveCatalog();
55626
+ const catalogTypes = new Set(catalog.map((e) => e.type.toLowerCase()));
55627
+ const aliases = new Set(catalog.flatMap((e) => (e.aliases ?? []).map((a) => a.toLowerCase())));
55628
+ const deviceTypes = [...new Set(Object.values(cache2.devices).map((d) => d.type))];
55629
+ const missing = deviceTypes.filter((t) => !catalogTypes.has(t.toLowerCase()) && !aliases.has(t.toLowerCase()));
55630
+ if (missing.length === 0) {
55631
+ return { name: "catalog-coverage", status: "ok", detail: `all ${deviceTypes.length} device types have catalog entries` };
55632
+ }
55633
+ return {
55634
+ name: "catalog-coverage",
55635
+ status: "warn",
55636
+ detail: { missing, message: `${missing.length} device type(s) without catalog entry` }
55637
+ };
55638
+ }
55544
55639
  function checkCache() {
55545
55640
  try {
55546
55641
  const info = describeCache();
@@ -55642,6 +55737,7 @@ function checkInventoryConsistency() {
55642
55737
  status: "warn",
55643
55738
  detail: {
55644
55739
  message: `${dangling.length} device(s) reference a hubDeviceId that is not present in the current inventory`,
55740
+ hint: "This usually means the hub was removed or replaced. Re-pair affected devices in the SwitchBot app, or ignore if the devices still work.",
55645
55741
  dangling: dangling.slice(0, 10)
55646
55742
  }
55647
55743
  };
@@ -56143,6 +56239,7 @@ var CHECK_REGISTRY = [
56143
56239
  { name: "keychain", description: "OS keychain backend availability and usage", run: () => checkKeychain() },
56144
56240
  { name: "profiles", description: "profile definitions valid", run: () => checkProfiles() },
56145
56241
  { name: "catalog", description: "catalog loads", run: () => checkCatalog() },
56242
+ { name: "catalog-coverage", description: "all cached device types have catalog entries", run: () => checkCatalogCoverage() },
56146
56243
  { name: "catalog-schema", description: "catalog vs agent-bootstrap version aligned", run: () => checkCatalogSchema() },
56147
56244
  { name: "inventory", description: "cached inventory graph consistency (hubDeviceId references)", run: () => checkInventoryConsistency() },
56148
56245
  { name: "cache", description: "device cache state", run: () => checkCache() },
@@ -56204,7 +56301,7 @@ function applyFixes(checks, writeOk) {
56204
56301
  return results;
56205
56302
  }
56206
56303
  function registerDoctorCommand(program3) {
56207
- program3.command("doctor").description("Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP").option("--section <names>", "Comma-separated list of checks to run (see --list for names)").option("--list", "Print the registered check names and exit 0 without running any check").option("--fix", "Apply safe, reversible remediations for failing checks (e.g. clear stale cache)").option("--yes", "Required together with --fix to confirm write actions").option("--probe", "Perform live-probe variant of checks that support it (mqtt)").addHelpText("after", `
56304
+ program3.command("doctor").description("Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP").option("--section <names>", "Comma-separated list of checks to run (see --list for names)").option("--list", "Print the registered check names and exit 0 without running any check").option("--fix", "Apply safe, reversible remediations for failing checks (e.g. clear stale cache)").option("--yes", "Required together with --fix to confirm write actions").option("--probe", "Perform live-probe variant of checks that support it (mqtt)").option("-q, --quiet", "Only show warn/fail checks, hide passing checks").addHelpText("after", `
56208
56305
  Runs a battery of local sanity checks and exits with code 0 only when every
56209
56306
  check is 'ok'. 'warn' \u2192 exit 0 (informational); 'fail' \u2192 exit 1.
56210
56307
 
@@ -56281,7 +56378,9 @@ Examples:
56281
56378
  if (fixes !== void 0) payload.fixes = fixes;
56282
56379
  printJson(payload);
56283
56380
  } else {
56381
+ const quiet = Boolean(opts.quiet);
56284
56382
  for (const c of checks) {
56383
+ if (quiet && c.status === "ok") continue;
56285
56384
  const icon2 = c.status === "ok" ? "\u2713" : c.status === "warn" ? "!" : "\u2717";
56286
56385
  const detailStr = typeof c.detail === "string" ? c.detail : typeof c.detail.message === "string" ? c.detail.message : JSON.stringify(c.detail);
56287
56386
  console.log(`${icon2} ${c.name.padEnd(12)} ${detailStr}`);
@@ -57114,8 +57213,8 @@ Examples:
57114
57213
  if (opts.dryRun) {
57115
57214
  if (isJsonMode()) printJson(finalPayload);
57116
57215
  else {
57117
- console.log(`\u2022 dry-run: would upgrade ${policyPath} (v${plan.fromVersion} \u2192 v${plan.toVersion})`);
57118
- console.log(` bytes: ${bytesWritten}`);
57216
+ console.error(`\u2022 dry-run: would upgrade ${policyPath} (v${plan.fromVersion} \u2192 v${plan.toVersion})`);
57217
+ console.error(` bytes: ${bytesWritten}`);
57119
57218
  console.log(` precheck: valid against v${target}`);
57120
57219
  }
57121
57220
  return;
@@ -57232,7 +57331,7 @@ Reads rule YAML from stdin. Combine with 'rules suggest' for a full pipeline:
57232
57331
  if (result.written) {
57233
57332
  console.log(`\u2713 rule "${result.ruleName}" added to ${policyPath}`);
57234
57333
  } else {
57235
- console.log(`\u2022 dry-run: rule "${result.ruleName}" not written`);
57334
+ console.error(`\u2022 dry-run: rule "${result.ruleName}" not written`);
57236
57335
  }
57237
57336
  }
57238
57337
  } catch (err) {
@@ -59312,18 +59411,18 @@ function printDryRun(steps, ctx) {
59312
59411
  });
59313
59412
  return;
59314
59413
  }
59315
- console.log(source_default.bold("switchbot install \u2014 dry run"));
59316
- console.log(` profile: ${ctx.profile}`);
59317
- console.log(` agent: ${ctx.agent}`);
59318
- console.log(` skill: ${ctx.skillPath ?? "(none \u2014 recipe will be printed)"}`);
59319
- console.log(` policy: ${ctx.policyPath}`);
59320
- console.log("");
59321
- console.log(source_default.bold("Steps that would run (in order):"));
59414
+ console.error(source_default.bold("switchbot install \u2014 dry run"));
59415
+ console.error(` profile: ${ctx.profile}`);
59416
+ console.error(` agent: ${ctx.agent}`);
59417
+ console.error(` skill: ${ctx.skillPath ?? "(none \u2014 recipe will be printed)"}`);
59418
+ console.error(` policy: ${ctx.policyPath}`);
59419
+ console.error("");
59420
+ console.error(source_default.bold("Steps that would run (in order):"));
59322
59421
  for (const s2 of steps) {
59323
- console.log(` \u2022 ${s2.name}${s2.description ? ` \u2014 ${s2.description}` : ""}`);
59422
+ console.error(` \u2022 ${s2.name}${s2.description ? ` \u2014 ${s2.description}` : ""}`);
59324
59423
  }
59325
- console.log("");
59326
- console.log(source_default.dim("No changes made. Re-run without --dry-run to apply."));
59424
+ console.error("");
59425
+ console.error(source_default.dim("No changes made. Re-run without --dry-run to apply."));
59327
59426
  }
59328
59427
  function registerInstallCommand(program3) {
59329
59428
  program3.command("install").description("One-command bootstrap: credentials + policy + skill link (rolls back on failure)").option("--agent <name>", `target agent: ${AGENT_VALUES.join(" | ")} (default: claude-code)`).option("--skill-path <dir>", "local clone of openclaw-switchbot-skill (enables auto-link)").option("--token-file <path>", "two-line credential file (token, secret); read once and deleted on success").option("--skip <names>", 'comma-separated list of step names to skip (e.g. "scaffold-policy,symlink-skill")').option("--force", "replace an existing skill symlink pointing at a different path; allow link even without SKILL.md").option("--verify", "after a successful install, run `switchbot doctor --json` as a warn-only post-check").addHelpText(
@@ -59618,14 +59717,14 @@ Examples:
59618
59717
  plan: plan.map(({ action, detail }) => ({ action, detail }))
59619
59718
  });
59620
59719
  } else {
59621
- console.log(source_default.bold("switchbot uninstall \u2014 dry run"));
59622
- console.log(` profile: ${profile}`);
59623
- console.log(` agent: ${agent}`);
59624
- console.log("");
59625
- console.log(source_default.bold("Would run:"));
59626
- for (const p2 of plan) console.log(` \u2022 ${p2.action} \u2014 ${p2.detail}`);
59627
- console.log("");
59628
- console.log(source_default.dim("No changes made. Re-run without --dry-run (add --yes to skip prompts)."));
59720
+ console.error(source_default.bold("switchbot uninstall \u2014 dry run"));
59721
+ console.error(` profile: ${profile}`);
59722
+ console.error(` agent: ${agent}`);
59723
+ console.error("");
59724
+ console.error(source_default.bold("Would run:"));
59725
+ for (const p2 of plan) console.error(` \u2022 ${p2.action} \u2014 ${p2.detail}`);
59726
+ console.error("");
59727
+ console.error(source_default.dim("No changes made. Re-run without --dry-run (add --yes to skip prompts)."));
59629
59728
  }
59630
59729
  return;
59631
59730
  }
@@ -60201,7 +60300,7 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
60201
60300
  const breakdown = {};
60202
60301
  let expectedErrors = 0;
60203
60302
  for (const e of errorEntries) {
60204
- const code = e.statusCode !== void 0 ? String(e.statusCode) : "unknown";
60303
+ const code = e.statusCode !== void 0 ? String(e.statusCode) : "client";
60205
60304
  breakdown[code] = (breakdown[code] ?? 0) + 1;
60206
60305
  if (e.statusCode !== void 0 && EXPECTED_ERROR_CODES.has(e.statusCode)) {
60207
60306
  expectedErrors++;
@@ -60268,38 +60367,46 @@ function runHealthCheck(opts) {
60268
60367
  process.stdout.write(toPrometheusText(report));
60269
60368
  return;
60270
60369
  }
60271
- if (isJsonMode()) {
60370
+ const fmt = resolveFormat();
60371
+ if (fmt === "json" || isJsonMode()) {
60272
60372
  printJson(report);
60273
60373
  return;
60274
60374
  }
60375
+ const headers = ["Component", "Status", "Detail"];
60376
+ const rows = [
60377
+ [
60378
+ "quota",
60379
+ report.quota.status,
60380
+ `${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`
60381
+ ],
60382
+ [
60383
+ "audit",
60384
+ report.audit.status,
60385
+ report.audit.present ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)` : "log not present"
60386
+ ],
60387
+ [
60388
+ "circuit",
60389
+ report.circuit.status,
60390
+ `${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`
60391
+ ],
60392
+ [
60393
+ "process",
60394
+ "ok",
60395
+ `pid ${report.process.pid} \xB7 uptime ${report.process.uptimeSeconds}s \xB7 mem ${report.process.memoryMb}MB`
60396
+ ]
60397
+ ];
60398
+ if (fmt !== "table") {
60399
+ if (fmt === "id") {
60400
+ handleError(new UsageError("--format=id is not supported for health check (no deviceId column). Use --format json, yaml, tsv, jsonl, or markdown."));
60401
+ }
60402
+ renderRows(headers, rows, fmt);
60403
+ if (report.overall !== "ok") process.exit(1);
60404
+ return;
60405
+ }
60275
60406
  const statusEmoji = report.overall === "ok" ? "\u2713" : report.overall === "degraded" ? "\u26A0" : "\u2717";
60276
60407
  console.log(`${statusEmoji} overall: ${report.overall} (${report.generatedAt})`);
60277
60408
  console.log("");
60278
- printTable(
60279
- ["Component", "Status", "Detail"],
60280
- [
60281
- [
60282
- "quota",
60283
- report.quota.status,
60284
- `${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`
60285
- ],
60286
- [
60287
- "audit",
60288
- report.audit.status,
60289
- report.audit.present ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)` : "log not present"
60290
- ],
60291
- [
60292
- "circuit",
60293
- report.circuit.status,
60294
- `${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`
60295
- ],
60296
- [
60297
- "process",
60298
- "ok",
60299
- `pid ${report.process.pid} \xB7 uptime ${report.process.uptimeSeconds}s \xB7 mem ${report.process.memoryMb}MB`
60300
- ]
60301
- ]
60302
- );
60409
+ printTable(headers, rows);
60303
60410
  if (report.overall !== "ok") process.exit(1);
60304
60411
  }
60305
60412
  function createHealthHandler(auditLogPath) {
@@ -60853,6 +60960,7 @@ if (process.argv.includes("--no-color") || Boolean(process.env.NO_COLOR)) {
60853
60960
  source_default.level = 0;
60854
60961
  }
60855
60962
  var program2 = new Command();
60963
+ program2.allowExcessArguments(false);
60856
60964
  if (isJsonMode()) {
60857
60965
  program2.configureOutput({ writeErr: () => {
60858
60966
  } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "3.6.0",
3
+ "version": "3.6.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",
@@ -42,6 +42,7 @@
42
42
  "hooks:install": "node scripts/install-git-hooks.mjs",
43
43
  "lint:md": "markdownlint \"**/*.md\"",
44
44
  "lint:md:changelog": "markdownlint CHANGELOG.md",
45
+ "lint:stdout": "bash scripts/lint-stdout.sh",
45
46
  "prepare": "node scripts/install-git-hooks.mjs",
46
47
  "start": "node dist/index.js",
47
48
  "smoke:pack-install": "node scripts/smoke-pack-install.mjs",
@@ -51,6 +52,7 @@
51
52
  "test:release-smoke:manual": "npm test -- tests/commands/policy.test.ts tests/commands/devices.test.ts tests/commands/explain.test.ts tests/commands/doctor.test.ts tests/commands/mcp.test.ts tests/commands/health-check.test.ts tests/commands/quota.test.ts tests/commands/status-sync.test.ts tests/status-sync/smoke.test.ts tests/commands/watch.test.ts tests/commands/events.test.ts tests/devices/catalog-fidelity.test.ts tests/commands/schema.test.ts tests/commands/auth.test.ts tests/commands/config.test.ts tests/commands/scenes.test.ts tests/commands/batch.test.ts tests/commands/history.test.ts tests/commands/expand.test.ts tests/commands/webhook.test.ts tests/commands/daemon.test.ts tests/commands/upgrade-check.test.ts tests/commands/install.test.ts tests/commands/uninstall.test.ts tests/commands/rules.test.ts tests/commands/plan.test.ts",
52
53
  "verify:pre-commit": "npm run build && npm test -- tests/version.test.ts",
53
54
  "verify:pre-push": "npm run build && npm test -- tests/version.test.ts && npm run smoke:pack-install",
55
+ "verify:release": "node scripts/verify-release.mjs",
54
56
  "prepublishOnly": "npm test && npm run build && npm run smoke:pack-install"
55
57
  },
56
58
  "dependencies": {