@switchbot/openapi-cli 2.6.4 → 2.7.2

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.
@@ -1,5 +1,20 @@
1
1
  import { UsageError } from '../utils/output.js';
2
+ /**
3
+ * User-facing aliases for canonical field names.
4
+ *
5
+ * Keys are canonical names (matching API response keys and CLI/schema output);
6
+ * values are lowercase alternatives a user may type for `--fields` or `--filter`.
7
+ *
8
+ * Conflict rules (do not add an alias that violates these — tests will fail):
9
+ * - `temp` is exclusive to `temperature` (NOT `colorTemperature`, `targetTemperature`).
10
+ * - `motion` is exclusive to `moveDetected`; `moving` uses `active` instead.
11
+ * - `mode` is exclusive to top-level `mode` (preset); device-specific modes go through `deviceMode`.
12
+ * - Reserved / too-generic words never appear as aliases: `auto`, `status`, `state`,
13
+ * `switch`, `type`, `on`, `off`.
14
+ * - Device-type words are never aliases: `lock`, `fan`.
15
+ */
2
16
  export const FIELD_ALIASES = {
17
+ // Identification (shared with list/filter)
3
18
  deviceId: ['id'],
4
19
  deviceName: ['name'],
5
20
  deviceType: ['type'],
@@ -10,7 +25,71 @@ export const FIELD_ALIASES = {
10
25
  hubDeviceId: ['hub'],
11
26
  enableCloudService: ['cloud'],
12
27
  alias: ['alias'],
28
+ // Phase 1 — common status fields
29
+ battery: ['batt', 'bat'],
30
+ temperature: ['temp', 'ambient'],
31
+ colorTemperature: ['kelvin', 'colortemp'],
32
+ humidity: ['humid', 'rh'],
33
+ brightness: ['bright', 'bri'],
34
+ fanSpeed: ['speed'],
35
+ position: ['pos'],
36
+ moveDetected: ['motion'],
37
+ openState: ['open'],
38
+ doorState: ['door'],
39
+ CO2: ['co2'],
40
+ power: ['enabled'],
41
+ mode: ['preset'],
42
+ // Phase 2 — niche device fields
43
+ childLock: ['safe', 'childlock'],
44
+ targetTemperature: ['setpoint', 'target'],
45
+ electricCurrent: ['current', 'amps'],
46
+ voltage: ['volts'],
47
+ usedElectricity: ['energy', 'kwh'],
48
+ electricityOfDay: ['daily', 'today'],
49
+ weight: ['load'],
50
+ version: ['firmware', 'fw'],
51
+ lightLevel: ['light', 'lux'],
52
+ oscillation: ['swing', 'osc'],
53
+ verticalOscillation: ['vswing'],
54
+ nightStatus: ['night'],
55
+ chargingStatus: ['charging', 'charge'],
56
+ switch1Status: ['ch1', 'channel1'],
57
+ switch2Status: ['ch2', 'channel2'],
58
+ taskType: ['task'],
59
+ moving: ['active'],
60
+ onlineStatus: ['online_status'],
61
+ workingStatus: ['working'],
62
+ // Phase 3 — catalog statusFields coverage
63
+ group: ['cluster'],
64
+ calibrate: ['calibration', 'calib'],
65
+ direction: ['tilt'],
66
+ deviceMode: ['devmode'],
67
+ nebulizationEfficiency: ['mist', 'spray'],
68
+ sound: ['audio'],
69
+ lackWater: ['tank', 'water-low'],
70
+ filterElement: ['filter'],
71
+ color: ['rgb', 'hex'],
72
+ useTime: ['runtime', 'uptime'],
73
+ switchStatus: ['relay'],
74
+ lockState: ['locked'],
75
+ slidePosition: ['slide'],
76
+ // Phase 4 — ultra-niche sensor + webhook fields (~98% coverage target)
77
+ waterLeakDetect: ['leak', 'water'],
78
+ pressure: ['press', 'pa'],
79
+ moveCount: ['movecnt'],
80
+ errorCode: ['err'],
81
+ buttonName: ['btn', 'button'],
82
+ pressedAt: ['pressed'],
83
+ deviceMac: ['mac'],
84
+ detectionState: ['detected', 'detect'],
13
85
  };
86
+ /**
87
+ * Resolve a user-typed field name to its canonical form against an allowed list.
88
+ *
89
+ * Matching is case-insensitive and trims surrounding whitespace. Direct matches
90
+ * win over alias matches. Throws UsageError if the input is empty or does not
91
+ * match any canonical / alias in the allowed list.
92
+ */
14
93
  export function resolveField(input, allowedCanonical) {
15
94
  const normalized = input.trim().toLowerCase();
16
95
  if (!normalized) {
@@ -19,12 +98,21 @@ export function resolveField(input, allowedCanonical) {
19
98
  for (const canonical of allowedCanonical) {
20
99
  if (canonical.toLowerCase() === normalized)
21
100
  return canonical;
101
+ }
102
+ for (const canonical of allowedCanonical) {
22
103
  const aliases = FIELD_ALIASES[canonical] ?? [];
23
104
  if (aliases.some((a) => a.toLowerCase() === normalized))
24
105
  return canonical;
25
106
  }
26
107
  throw new UsageError(`Unknown field "${input}". Supported: ${listSupportedFieldInputs(allowedCanonical).join(', ')}`);
27
108
  }
109
+ /**
110
+ * Resolve every field in a list. Preserves order and the original UsageError
111
+ * from resolveField() on the first unknown input.
112
+ */
113
+ export function resolveFieldList(inputs, allowedCanonical) {
114
+ return inputs.map((f) => resolveField(f, allowedCanonical));
115
+ }
28
116
  export function listSupportedFieldInputs(allowedCanonical) {
29
117
  const out = new Set();
30
118
  for (const canonical of allowedCanonical) {
@@ -34,3 +122,10 @@ export function listSupportedFieldInputs(allowedCanonical) {
34
122
  }
35
123
  return [...out];
36
124
  }
125
+ /**
126
+ * All canonical keys known to the alias registry. Use when no dynamic
127
+ * canonical list is available (e.g. `watch` before the first poll response).
128
+ */
129
+ export function listAllCanonical() {
130
+ return Object.keys(FIELD_ALIASES);
131
+ }
@@ -0,0 +1,54 @@
1
+ import { IDENTITY } from '../commands/identity.js';
2
+ export function commandToJson(cmd, opts = {}) {
3
+ const args = cmd.registeredArguments.map((a) => ({
4
+ name: a.name(),
5
+ required: a.required,
6
+ variadic: a.variadic,
7
+ description: a.description ?? '',
8
+ }));
9
+ const options = cmd.options
10
+ .filter((o) => o.long !== '--help' && o.long !== '--version')
11
+ .map((o) => {
12
+ const entry = { flags: o.flags, description: o.description ?? '' };
13
+ if (o.defaultValue !== undefined)
14
+ entry.defaultValue = o.defaultValue;
15
+ if (o.argChoices && o.argChoices.length > 0)
16
+ entry.choices = o.argChoices;
17
+ return entry;
18
+ });
19
+ const subcommands = cmd.commands
20
+ .filter((c) => !c.name().startsWith('_'))
21
+ .map((c) => ({ name: c.name(), description: c.description() }));
22
+ const out = {
23
+ name: cmd.name(),
24
+ description: cmd.description(),
25
+ arguments: args,
26
+ options,
27
+ subcommands,
28
+ };
29
+ if (opts.includeIdentity) {
30
+ out.product = IDENTITY.product;
31
+ out.domain = IDENTITY.domain;
32
+ out.vendor = IDENTITY.vendor;
33
+ out.apiVersion = IDENTITY.apiVersion;
34
+ out.apiDocs = IDENTITY.apiDocs;
35
+ out.productCategories = IDENTITY.productCategories;
36
+ }
37
+ return out;
38
+ }
39
+ /** Walk argv tokens (skipping flags) to find the deepest matching subcommand. */
40
+ export function resolveTargetCommand(root, argv) {
41
+ let cmd = root;
42
+ for (const token of argv) {
43
+ if (token.startsWith('-'))
44
+ continue;
45
+ const sub = cmd.commands.find((c) => c.name() === token || c.aliases().includes(token));
46
+ if (sub) {
47
+ cmd = sub;
48
+ }
49
+ else {
50
+ break;
51
+ }
52
+ }
53
+ return cmd;
54
+ }
@@ -28,6 +28,23 @@ export function emitJsonError(errorPayload) {
28
28
  console.error(chalk.red(msg));
29
29
  }
30
30
  }
31
+ /**
32
+ * P7: emit the stream-header first line for any NDJSON/streaming command
33
+ * running under `--json`. Downstream JSON consumers can key on
34
+ * `{ stream: true }` to distinguish the header from subsequent event
35
+ * lines, and on `eventKind` / `cadence` to pick a parser strategy.
36
+ *
37
+ * Non-streaming commands (single-object / array output) do NOT emit this
38
+ * header — only watch / events tail / events mqtt-tail.
39
+ */
40
+ export function emitStreamHeader(opts) {
41
+ console.log(JSON.stringify({
42
+ schemaVersion: '1',
43
+ stream: true,
44
+ eventKind: opts.eventKind,
45
+ cadence: opts.cadence,
46
+ }));
47
+ }
31
48
  export function exitWithError(messageOrOpts) {
32
49
  const opts = typeof messageOrOpts === 'string' ? { message: messageOrOpts } : messageOrOpts;
33
50
  const { message, kind = 'usage', code = 2, hint, context, extra } = opts;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchbot/openapi-cli",
3
- "version": "2.6.4",
3
+ "version": "2.7.2",
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",