@switchbot/openapi-cli 3.4.1 โ†’ 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +259 -37
  3. package/package.json +1 -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** โ€” 2225 Vitest tests, mocked axios, zero network in CI
66
+ - ๐Ÿงช **Fully tested** โ€” 2263 Vitest tests, mocked axios, zero network in CI
67
67
  - โšก **Shell completion** โ€” Bash / Zsh / Fish / PowerShell
68
68
 
69
69
  ## Requirements
package/dist/index.js CHANGED
@@ -8380,9 +8380,8 @@ function updateCacheFromDeviceList(body) {
8380
8380
  for (const d of body.deviceList) {
8381
8381
  if (!d.deviceId) continue;
8382
8382
  devices[d.deviceId] = {
8383
- // Some real devices omit deviceType entirely (for example AI accessories).
8384
- // Keep them in cache with an empty type string rather than dropping the row.
8385
- type: d.deviceType ?? "",
8383
+ type: d.deviceType || d.controlType || "Unknown Device",
8384
+ typeSource: d.deviceType ? "deviceType" : d.controlType ? "controlType" : "deviceType",
8386
8385
  name: d.deviceName,
8387
8386
  category: "physical",
8388
8387
  hubDeviceId: d.hubDeviceId,
@@ -8397,6 +8396,7 @@ function updateCacheFromDeviceList(body) {
8397
8396
  if (!d.deviceId) continue;
8398
8397
  devices[d.deviceId] = {
8399
8398
  type: d.remoteType,
8399
+ typeSource: "remoteType",
8400
8400
  name: d.deviceName,
8401
8401
  category: "ir",
8402
8402
  hubDeviceId: d.hubDeviceId,
@@ -8793,10 +8793,11 @@ async function fetchDeviceList(client, options = {}) {
8793
8793
  const infraredRemoteList = [];
8794
8794
  for (const [deviceId, entry] of Object.entries(cached2.devices)) {
8795
8795
  if (entry.category === "physical") {
8796
+ const cachedDeviceType = entry.typeSource === "deviceType" ? entry.type : entry.typeSource === void 0 && entry.type !== entry.controlType ? entry.type : void 0;
8796
8797
  deviceList.push({
8797
8798
  deviceId,
8798
8799
  deviceName: entry.name,
8799
- ...entry.type ? { deviceType: entry.type } : {},
8800
+ ...cachedDeviceType && cachedDeviceType !== "Unknown Device" ? { deviceType: cachedDeviceType } : {},
8800
8801
  enableCloudService: entry.enableCloudService ?? true,
8801
8802
  hubDeviceId: entry.hubDeviceId ?? "",
8802
8803
  roomID: entry.roomID,
@@ -8867,10 +8868,12 @@ async function executeCommand(deviceId, cmd, parameter, commandType, client, opt
8867
8868
  if (err instanceof Error && err.name === "DryRunSignal") {
8868
8869
  writeAudit({ ...baseAudit, result: "dry-run" });
8869
8870
  } else {
8871
+ const statusCode = err instanceof ApiError ? err.code : void 0;
8870
8872
  writeAudit({
8871
8873
  ...baseAudit,
8872
8874
  result: "error",
8873
- error: err instanceof Error ? err.message : String(err)
8875
+ error: err instanceof Error ? err.message : String(err),
8876
+ ...statusCode !== void 0 ? { statusCode } : {}
8874
8877
  });
8875
8878
  }
8876
8879
  throw err;
@@ -8991,7 +8994,23 @@ async function describeDevice(deviceId, options = {}, client) {
8991
8994
  ir = infraredRemoteList.find((d) => d.deviceId === deviceId);
8992
8995
  }
8993
8996
  if (!physical && !ir) throw new DeviceNotFoundError(deviceId);
8994
- const typeName = physical ? physical.deviceType ?? "" : ir.remoteType;
8997
+ let typeName;
8998
+ let typeSource;
8999
+ if (physical) {
9000
+ if (physical.deviceType) {
9001
+ typeName = physical.deviceType;
9002
+ typeSource = "deviceType";
9003
+ } else if (physical.controlType) {
9004
+ typeName = physical.controlType;
9005
+ typeSource = "controlType";
9006
+ } else {
9007
+ typeName = "Unknown Device";
9008
+ typeSource = "deviceType";
9009
+ }
9010
+ } else {
9011
+ typeName = ir.remoteType;
9012
+ typeSource = "remoteType";
9013
+ }
8995
9014
  const match = typeName ? findCatalogEntry(typeName) : null;
8996
9015
  const catalogEntry = !match || Array.isArray(match) ? null : match;
8997
9016
  let liveStatus;
@@ -9027,6 +9046,7 @@ async function describeDevice(deviceId, options = {}, client) {
9027
9046
  device: selectedDevice,
9028
9047
  isPhysical: Boolean(physical),
9029
9048
  typeName,
9049
+ typeSource,
9030
9050
  controlType: physical?.controlType ?? ir?.controlType ?? null,
9031
9051
  catalog: catalogEntry,
9032
9052
  capabilities,
@@ -31710,6 +31730,161 @@ function applyFilter(clauses, deviceList, infraredRemoteList, hubLocation) {
31710
31730
  // src/devices/param-validator.ts
31711
31731
  init_cjs_shim();
31712
31732
  init_output();
31733
+
31734
+ // src/devices/css-colors.ts
31735
+ init_cjs_shim();
31736
+ var CSS_COLORS = {
31737
+ aliceblue: [240, 248, 255],
31738
+ antiquewhite: [250, 235, 215],
31739
+ aqua: [0, 255, 255],
31740
+ aquamarine: [127, 255, 212],
31741
+ azure: [240, 255, 255],
31742
+ beige: [245, 245, 220],
31743
+ bisque: [255, 228, 196],
31744
+ black: [0, 0, 0],
31745
+ blanchedalmond: [255, 235, 205],
31746
+ blue: [0, 0, 255],
31747
+ blueviolet: [138, 43, 226],
31748
+ brown: [165, 42, 42],
31749
+ burlywood: [222, 184, 135],
31750
+ cadetblue: [95, 158, 160],
31751
+ chartreuse: [127, 255, 0],
31752
+ chocolate: [210, 105, 30],
31753
+ coral: [255, 127, 80],
31754
+ cornflowerblue: [100, 149, 237],
31755
+ cornsilk: [255, 248, 220],
31756
+ crimson: [220, 20, 60],
31757
+ cyan: [0, 255, 255],
31758
+ darkblue: [0, 0, 139],
31759
+ darkcyan: [0, 139, 139],
31760
+ darkgoldenrod: [184, 134, 11],
31761
+ darkgray: [169, 169, 169],
31762
+ darkgreen: [0, 100, 0],
31763
+ darkgrey: [169, 169, 169],
31764
+ darkkhaki: [189, 183, 107],
31765
+ darkmagenta: [139, 0, 139],
31766
+ darkolivegreen: [85, 107, 47],
31767
+ darkorange: [255, 140, 0],
31768
+ darkorchid: [153, 50, 204],
31769
+ darkred: [139, 0, 0],
31770
+ darksalmon: [233, 150, 122],
31771
+ darkseagreen: [143, 188, 143],
31772
+ darkslateblue: [72, 61, 139],
31773
+ darkslategray: [47, 79, 79],
31774
+ darkslategrey: [47, 79, 79],
31775
+ darkturquoise: [0, 206, 209],
31776
+ darkviolet: [148, 0, 211],
31777
+ deeppink: [255, 20, 147],
31778
+ deepskyblue: [0, 191, 255],
31779
+ dimgray: [105, 105, 105],
31780
+ dimgrey: [105, 105, 105],
31781
+ dodgerblue: [30, 144, 255],
31782
+ firebrick: [178, 34, 34],
31783
+ floralwhite: [255, 250, 240],
31784
+ forestgreen: [34, 139, 34],
31785
+ fuchsia: [255, 0, 255],
31786
+ gainsboro: [220, 220, 220],
31787
+ ghostwhite: [248, 248, 255],
31788
+ gold: [255, 215, 0],
31789
+ goldenrod: [218, 165, 32],
31790
+ gray: [128, 128, 128],
31791
+ green: [0, 128, 0],
31792
+ greenyellow: [173, 255, 47],
31793
+ grey: [128, 128, 128],
31794
+ honeydew: [240, 255, 240],
31795
+ hotpink: [255, 105, 180],
31796
+ indianred: [205, 92, 92],
31797
+ indigo: [75, 0, 130],
31798
+ ivory: [255, 255, 240],
31799
+ khaki: [240, 230, 140],
31800
+ lavender: [230, 230, 250],
31801
+ lavenderblush: [255, 240, 245],
31802
+ lawngreen: [124, 252, 0],
31803
+ lemonchiffon: [255, 250, 205],
31804
+ lightblue: [173, 216, 230],
31805
+ lightcoral: [240, 128, 128],
31806
+ lightcyan: [224, 255, 255],
31807
+ lightgoldenrodyellow: [250, 250, 210],
31808
+ lightgray: [211, 211, 211],
31809
+ lightgreen: [144, 238, 144],
31810
+ lightgrey: [211, 211, 211],
31811
+ lightpink: [255, 182, 193],
31812
+ lightsalmon: [255, 160, 122],
31813
+ lightseagreen: [32, 178, 170],
31814
+ lightskyblue: [135, 206, 250],
31815
+ lightslategray: [119, 136, 153],
31816
+ lightslategrey: [119, 136, 153],
31817
+ lightsteelblue: [176, 196, 222],
31818
+ lightyellow: [255, 255, 224],
31819
+ lime: [0, 255, 0],
31820
+ limegreen: [50, 205, 50],
31821
+ linen: [250, 240, 230],
31822
+ magenta: [255, 0, 255],
31823
+ maroon: [128, 0, 0],
31824
+ mediumaquamarine: [102, 205, 170],
31825
+ mediumblue: [0, 0, 205],
31826
+ mediumorchid: [186, 85, 211],
31827
+ mediumpurple: [147, 111, 219],
31828
+ mediumseagreen: [60, 179, 113],
31829
+ mediumslateblue: [123, 104, 238],
31830
+ mediumspringgreen: [0, 250, 154],
31831
+ mediumturquoise: [72, 209, 204],
31832
+ mediumvioletred: [199, 21, 133],
31833
+ midnightblue: [25, 25, 112],
31834
+ mintcream: [245, 255, 250],
31835
+ mistyrose: [255, 228, 225],
31836
+ moccasin: [255, 228, 181],
31837
+ navajowhite: [255, 222, 173],
31838
+ navy: [0, 0, 128],
31839
+ oldlace: [253, 245, 230],
31840
+ olive: [128, 128, 0],
31841
+ olivedrab: [107, 142, 35],
31842
+ orange: [255, 165, 0],
31843
+ orangered: [255, 69, 0],
31844
+ orchid: [218, 112, 214],
31845
+ palegoldenrod: [238, 232, 170],
31846
+ palegreen: [152, 251, 152],
31847
+ paleturquoise: [175, 238, 238],
31848
+ palevioletred: [219, 112, 147],
31849
+ papayawhip: [255, 239, 213],
31850
+ peachpuff: [255, 218, 185],
31851
+ peru: [205, 133, 63],
31852
+ pink: [255, 192, 203],
31853
+ plum: [221, 160, 221],
31854
+ powderblue: [176, 224, 230],
31855
+ purple: [128, 0, 128],
31856
+ rebeccapurple: [102, 51, 153],
31857
+ red: [255, 0, 0],
31858
+ rosybrown: [188, 143, 143],
31859
+ royalblue: [65, 105, 225],
31860
+ saddlebrown: [139, 69, 19],
31861
+ salmon: [250, 128, 114],
31862
+ sandybrown: [244, 164, 96],
31863
+ seagreen: [46, 139, 87],
31864
+ seashell: [255, 245, 238],
31865
+ sienna: [160, 82, 45],
31866
+ silver: [192, 192, 192],
31867
+ skyblue: [135, 206, 235],
31868
+ slateblue: [106, 90, 205],
31869
+ slategray: [112, 128, 144],
31870
+ slategrey: [112, 128, 144],
31871
+ snow: [255, 250, 250],
31872
+ springgreen: [0, 255, 127],
31873
+ steelblue: [70, 130, 180],
31874
+ tan: [210, 180, 140],
31875
+ teal: [0, 128, 128],
31876
+ thistle: [216, 191, 216],
31877
+ tomato: [255, 99, 71],
31878
+ turquoise: [64, 224, 208],
31879
+ violet: [238, 130, 238],
31880
+ wheat: [245, 222, 179],
31881
+ white: [255, 255, 255],
31882
+ whitesmoke: [245, 245, 245],
31883
+ yellow: [255, 255, 0],
31884
+ yellowgreen: [154, 205, 50]
31885
+ };
31886
+
31887
+ // src/devices/param-validator.ts
31713
31888
  var AC_MODE_MAP = { auto: 1, cool: 2, dry: 3, fan: 4, heat: 5 };
31714
31889
  var AC_FAN_MAP = { auto: 1, low: 2, mid: 3, high: 4 };
31715
31890
  var CURTAIN_MODE_MAP = { default: "ff", performance: "0", silent: "1" };
@@ -31858,29 +32033,18 @@ function validateSetBrightness(raw) {
31858
32033
  function hintBrightnessRetry() {
31859
32034
  return `Ask the user whether they meant a percentage (1-100). Example: "50".`;
31860
32035
  }
31861
- var NAMED_COLORS = {
31862
- red: [255, 0, 0],
31863
- green: [0, 128, 0],
31864
- lime: [0, 255, 0],
31865
- blue: [0, 0, 255],
31866
- yellow: [255, 255, 0],
31867
- cyan: [0, 255, 255],
31868
- magenta: [255, 0, 255],
31869
- white: [255, 255, 255],
31870
- black: [0, 0, 0],
31871
- orange: [255, 165, 0],
31872
- purple: [128, 0, 128],
31873
- pink: [255, 192, 203],
31874
- brown: [165, 42, 42],
31875
- grey: [128, 128, 128],
31876
- gray: [128, 128, 128],
32036
+ var CUSTOM_COLORS = {
31877
32037
  warm: [255, 180, 100]
31878
32038
  };
32039
+ var NAMED_COLORS = {
32040
+ ...CSS_COLORS,
32041
+ ...CUSTOM_COLORS
32042
+ };
31879
32043
  function validateSetColor(raw) {
31880
32044
  if (raw === void 0 || raw === "" || raw === "default") {
31881
32045
  return {
31882
32046
  ok: false,
31883
- error: `setColor requires a color. Expected one of: "R:G:B" (e.g. "255:0:0"), "#RRGGBB" (e.g. "#FF0000"), "#RGB", "R,G,B", or a named color (${Object.keys(NAMED_COLORS).slice(0, 8).join(", ")}, ...).`
32047
+ error: `setColor requires a color. Use a CSS color name (e.g. coral, teal, salmon), hex (#RRGGBB / #RGB), or R:G:B format.`
31884
32048
  };
31885
32049
  }
31886
32050
  const trimmed = raw.trim();
@@ -32395,8 +32559,11 @@ Examples:
32395
32559
  }
32396
32560
  return;
32397
32561
  }
32562
+ const totalDevices = resolved.ids.length;
32563
+ const deviceIndices = new Map(resolved.ids.map((id, i) => [id, i + 1]));
32398
32564
  const startedAt = Date.now();
32399
32565
  const outcomes = await runPool(resolved.ids, concurrency, staggerMs, async (id) => {
32566
+ const stepIdx = deviceIndices.get(id);
32400
32567
  const stepStart = Date.now();
32401
32568
  const startedIso = new Date(stepStart).toISOString();
32402
32569
  try {
@@ -32408,7 +32575,7 @@ Examples:
32408
32575
  const durationMs = Date.now() - stepStart;
32409
32576
  const replayed = typeof result2 === "object" && result2 !== null && result2.replayed === true;
32410
32577
  if (!isJsonMode()) {
32411
- console.log(`\u2713 ${id}: ${cmd}${replayed ? " (replayed)" : ""}`);
32578
+ console.log(`[${stepIdx}/${totalDevices}] \u2713 ${id}: ${cmd}${replayed ? " (replayed)" : ""}`);
32412
32579
  }
32413
32580
  return {
32414
32581
  ok: true,
@@ -32431,7 +32598,7 @@ Examples:
32431
32598
  }
32432
32599
  const errorPayload = buildErrorPayload(err);
32433
32600
  if (!isJsonMode()) {
32434
- console.error(`\u2717 ${id}: ${errorPayload.message}`);
32601
+ console.error(`[${stepIdx}/${totalDevices}] \u2717 ${id}: ${errorPayload.message}`);
32435
32602
  }
32436
32603
  return {
32437
32604
  ok: false,
@@ -32681,7 +32848,7 @@ function registerWatchCommand(devices) {
32681
32848
  `Polling interval: "30s", "1m", "500ms", ... (default 30s, min ${MIN_INTERVAL_MS / 1e3}s)`,
32682
32849
  durationArg("--interval"),
32683
32850
  "30s"
32684
- ).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").option("--initial <mode>", "How to handle the first poll: snapshot | emit | skip (default: snapshot)", enumArg("--initial", INITIAL_MODES), "snapshot").addHelpText(
32851
+ ).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--once", "Stop after one tick (shorthand for --max 1)").option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").option("--initial <mode>", "How to handle the first poll: snapshot | emit | skip (default: snapshot)", enumArg("--initial", INITIAL_MODES), "snapshot").addHelpText(
32685
32852
  "after",
32686
32853
  `
32687
32854
  Default output is a human-readable table of field changes per tick; add --json
@@ -32710,6 +32877,12 @@ Examples:
32710
32877
  ).action(
32711
32878
  async (deviceIds, options) => {
32712
32879
  try {
32880
+ if (options.once && options.max !== void 0) {
32881
+ throw new UsageError("--once and --max are mutually exclusive.");
32882
+ }
32883
+ if (options.once) {
32884
+ options.max = "1";
32885
+ }
32713
32886
  const allIds = [...deviceIds];
32714
32887
  if (options.name) {
32715
32888
  const resolved = resolveDeviceId(void 0, options.name);
@@ -33258,13 +33431,16 @@ var EXPAND_HINTS = {
33258
33431
  "Relay Switch 2PM": { command: "setMode", flags: "--channel 1 --mode edge" }
33259
33432
  };
33260
33433
  function annotateStatusPayload(deviceId, body) {
33261
- const annotated = { ...body };
33434
+ const cached2 = getCachedDevice(deviceId);
33435
+ const deviceType = cached2?.type ?? "";
33436
+ const annotated = { deviceId, deviceType, ...body };
33437
+ annotated.deviceId = deviceId;
33438
+ annotated.deviceType = deviceType;
33262
33439
  if (Object.keys(body).length === 0) {
33263
33440
  annotated.supported = false;
33264
33441
  annotated.note = "this device does not expose cloud status";
33265
33442
  return annotated;
33266
33443
  }
33267
- const cached2 = getCachedDevice(deviceId);
33268
33444
  const looksLikeMeter = cached2?.type?.toLowerCase().includes("meter") ?? false;
33269
33445
  const staleZeroReading = looksLikeMeter && !Object.prototype.hasOwnProperty.call(body, "onlineStatus") && body.battery === 0 && body.temperature === 0 && body.humidity === 0;
33270
33446
  if (staleZeroReading) {
@@ -33421,7 +33597,7 @@ Examples:
33421
33597
  rows.push([
33422
33598
  d.deviceId,
33423
33599
  d.deviceName,
33424
- d.deviceType || "\u2014",
33600
+ d.deviceType || d.controlType || "Unknown Device",
33425
33601
  "physical",
33426
33602
  d.controlType || "\u2014",
33427
33603
  d.familyName || "\u2014",
@@ -33970,12 +34146,19 @@ Examples:
33970
34146
  });
33971
34147
  return;
33972
34148
  }
34149
+ if (result.typeSource === "controlType") {
34150
+ const deviceName2 = device.deviceName ?? deviceId;
34151
+ console.error(`warning: ${deviceName2} (${deviceId}): deviceType not reported by API, using controlType "${result.controlType}". Capabilities may be limited.`);
34152
+ } else if (typeName === "Unknown Device") {
34153
+ const deviceName2 = device.deviceName ?? deviceId;
34154
+ console.error(`warning: ${deviceName2} (${deviceId}): neither deviceType nor controlType reported by API. Capabilities may be limited.`);
34155
+ }
33973
34156
  if (isPhysical) {
33974
34157
  const physical = device;
33975
34158
  printKeyValue({
33976
34159
  deviceId: physical.deviceId,
33977
34160
  deviceName: physical.deviceName,
33978
- deviceType: physical.deviceType || "\u2014",
34161
+ deviceType: physical.deviceType || physical.controlType || "Unknown Device",
33979
34162
  controlType: physical.controlType || "\u2014",
33980
34163
  family: physical.familyName || "\u2014",
33981
34164
  roomID: physical.roomID || "\u2014",
@@ -34154,7 +34337,7 @@ Examples:
34154
34337
  handleError(error48);
34155
34338
  }
34156
34339
  });
34157
- scenes.command("execute").description("Execute a manual scene by its ID").argument("<sceneId>", 'Scene ID from "scenes list"').addHelpText("after", `
34340
+ scenes.command("execute").alias("run").description("Execute a manual scene by its ID").argument("<sceneId>", 'Scene ID from "scenes list"').addHelpText("after", `
34158
34341
  Example:
34159
34342
  $ switchbot scenes execute T12345678
34160
34343
  `).action(async (sceneId) => {
@@ -49887,15 +50070,27 @@ against the live API without executing any mutations.
49887
50070
  handleError(err);
49888
50071
  }
49889
50072
  });
49890
- plan.command("run").description("Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default").argument("[file]", 'Path to plan.json, or "-" / omit to read stdin').option("--yes", "Authorize destructive commands (e.g. Smart Lock unlock, Garage open)").option("--require-approval", "Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)").option("--continue-on-error", "Keep running after a failed step (default: stop at first error)").action(
50073
+ plan.command("run").description("Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default").argument("[file]", 'Path to plan.json, or "-" / omit to read stdin').option("--yes", "Authorize destructive commands (e.g. Smart Lock unlock, Garage open)").option("--require-approval", "Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)").option("--continue-on-error", "Keep running after a failed step (default: stop at first error)").option("--plan <json>", "Inline plan JSON (alternative to file argument or stdin)").action(
49891
50074
  async (file2, options) => {
49892
50075
  if (options.requireApproval && isJsonMode()) {
49893
50076
  console.error("error: --require-approval cannot be used with --json (no TTY available for prompts)");
49894
50077
  process.exit(1);
49895
50078
  }
50079
+ if (options.plan !== void 0 && file2 !== void 0) {
50080
+ console.error("error: --plan and a file argument are mutually exclusive.");
50081
+ process.exit(2);
50082
+ }
49896
50083
  let raw;
49897
50084
  try {
49898
- raw = await readPlanSource(file2);
50085
+ if (options.plan !== void 0) {
50086
+ try {
50087
+ raw = JSON.parse(options.plan);
50088
+ } catch (err) {
50089
+ throw new UsageError(`--plan is not valid JSON: ${err.message}`);
50090
+ }
50091
+ } else {
50092
+ raw = await readPlanSource(file2);
50093
+ }
49899
50094
  } catch (err) {
49900
50095
  handleError(err);
49901
50096
  }
@@ -55689,7 +55884,7 @@ Examples:
55689
55884
  $ switchbot history show --limit 10
55690
55885
  $ switchbot history replay 3
55691
55886
  `);
55692
- history.command("show").description("Print recent audit entries").option("--file <path>", `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg("--file")).option("--limit <n>", "Show only the last N entries", intArg("--limit", { min: 1 })).action((options) => {
55887
+ history.command("show").alias("list").description("Print recent audit entries").option("--file <path>", `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg("--file")).option("--limit <n>", "Show only the last N entries", intArg("--limit", { min: 1 })).action((options) => {
55693
55888
  const file2 = options.file ?? DEFAULT_AUDIT;
55694
55889
  const entries = readAudit(file2);
55695
55890
  const limited = options.limit !== void 0 ? entries.slice(-Math.max(1, Number(options.limit) || 1)) : entries;
@@ -59338,6 +59533,7 @@ import os25 from "node:os";
59338
59533
  import path27 from "node:path";
59339
59534
  var DEFAULT_AUDIT_PATH3 = path27.join(os25.homedir(), ".switchbot", "audit.log");
59340
59535
  var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
59536
+ var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
59341
59537
  function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
59342
59538
  const now = /* @__PURE__ */ new Date();
59343
59539
  const procHealth = {
@@ -59358,20 +59554,46 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
59358
59554
  };
59359
59555
  let auditHealth;
59360
59556
  if (!fs31.existsSync(auditPath)) {
59361
- auditHealth = { present: false, recentErrors: 0, recentTotal: 0, errorRatePercent: 0, status: "ok" };
59557
+ auditHealth = {
59558
+ present: false,
59559
+ recentErrors: 0,
59560
+ recentTotal: 0,
59561
+ errorRatePercent: 0,
59562
+ expectedErrors: 0,
59563
+ unexpectedErrors: 0,
59564
+ unexpectedRatePercent: 0,
59565
+ breakdown: {},
59566
+ status: "ok"
59567
+ };
59362
59568
  } else {
59363
59569
  const entries = readAudit(auditPath);
59364
59570
  const windowStart = now.getTime() - AUDIT_ERROR_WINDOW_MS;
59365
59571
  const recent = entries.filter((e) => new Date(e.t).getTime() >= windowStart);
59366
- const errors = recent.filter((e) => e.result === "error").length;
59572
+ const errorEntries = recent.filter((e) => e.result === "error");
59367
59573
  const total = recent.length;
59574
+ const errors = errorEntries.length;
59368
59575
  const errorRate = total > 0 ? Math.round(errors / total * 100) : 0;
59576
+ const breakdown = {};
59577
+ let expectedErrors = 0;
59578
+ for (const e of errorEntries) {
59579
+ const code = e.statusCode !== void 0 ? String(e.statusCode) : "unknown";
59580
+ breakdown[code] = (breakdown[code] ?? 0) + 1;
59581
+ if (e.statusCode !== void 0 && EXPECTED_ERROR_CODES.has(e.statusCode)) {
59582
+ expectedErrors++;
59583
+ }
59584
+ }
59585
+ const unexpectedErrors = errors - expectedErrors;
59586
+ const unexpectedRatePercent = total > 0 ? Math.round(unexpectedErrors / total * 100 * 10) / 10 : 0;
59369
59587
  auditHealth = {
59370
59588
  present: true,
59371
59589
  recentErrors: errors,
59372
59590
  recentTotal: total,
59373
59591
  errorRatePercent: errorRate,
59374
- status: errorRate >= 30 ? "warn" : "ok"
59592
+ expectedErrors,
59593
+ unexpectedErrors,
59594
+ unexpectedRatePercent,
59595
+ breakdown,
59596
+ status: unexpectedRatePercent >= 30 ? "warn" : "ok"
59375
59597
  };
59376
59598
  }
59377
59599
  const cbStats = apiCircuitBreaker.getStats();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "SwitchBot smart home CLI โ€” control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
5
5
  "keywords": [
6
6
  "switchbot",