@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.
- package/README.md +3 -3
- package/dist/index.js +217 -109
- 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** โ
|
|
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 (
|
|
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:
|
|
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: [
|
|
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
|
|
32225
|
-
if (
|
|
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 =
|
|
32232
|
-
if (
|
|
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
|
|
32325
|
-
if (
|
|
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 =
|
|
32332
|
-
if (
|
|
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
|
|
32363
|
-
if (!
|
|
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
|
|
32370
|
-
|
|
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
|
|
32377
|
-
|
|
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
|
|
32402
|
-
if (!
|
|
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(
|
|
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
|
|
32419
|
-
if (!
|
|
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
|
|
32433
|
-
if (!
|
|
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
|
|
32464
|
-
if (!
|
|
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
|
|
32494
|
-
if (
|
|
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
|
|
32501
|
-
|
|
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};${
|
|
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
|
|
32539
|
-
if (
|
|
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 =
|
|
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.
|
|
33278
|
+
console.error(`
|
|
33209
33279
|
Planned (dry-run): ${dryRunned.length} device(s)`);
|
|
33210
|
-
for (const d of dryRunned) console.
|
|
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.
|
|
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 (
|
|
34295
|
+
`).action(async (deviceIdArgs, options) => {
|
|
34225
34296
|
try {
|
|
34226
|
-
|
|
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
|
-
|
|
34229
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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>",
|
|
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>",
|
|
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>",
|
|
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.
|
|
57118
|
-
console.
|
|
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.
|
|
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.
|
|
59316
|
-
console.
|
|
59317
|
-
console.
|
|
59318
|
-
console.
|
|
59319
|
-
console.
|
|
59320
|
-
console.
|
|
59321
|
-
console.
|
|
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.
|
|
59422
|
+
console.error(` \u2022 ${s2.name}${s2.description ? ` \u2014 ${s2.description}` : ""}`);
|
|
59324
59423
|
}
|
|
59325
|
-
console.
|
|
59326
|
-
console.
|
|
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.
|
|
59622
|
-
console.
|
|
59623
|
-
console.
|
|
59624
|
-
console.
|
|
59625
|
-
console.
|
|
59626
|
-
for (const p2 of plan) console.
|
|
59627
|
-
console.
|
|
59628
|
-
console.
|
|
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) : "
|
|
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
|
-
|
|
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.
|
|
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": {
|