@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.
- package/README.md +2 -2
- package/dist/api/client.js +13 -12
- package/dist/commands/agent-bootstrap.js +21 -15
- package/dist/commands/batch.js +26 -21
- package/dist/commands/capabilities.js +29 -21
- package/dist/commands/catalog.js +4 -3
- package/dist/commands/config.js +27 -37
- package/dist/commands/devices.js +64 -37
- package/dist/commands/doctor.js +355 -19
- package/dist/commands/events.js +112 -23
- package/dist/commands/expand.js +7 -15
- package/dist/commands/explain.js +10 -6
- package/dist/commands/history.js +12 -18
- package/dist/commands/identity.js +59 -0
- package/dist/commands/mcp.js +100 -13
- package/dist/commands/plan.js +1 -1
- package/dist/commands/schema.js +22 -12
- package/dist/commands/watch.js +15 -2
- package/dist/devices/catalog.js +124 -11
- package/dist/devices/resources.js +270 -0
- package/dist/index.js +16 -3
- package/dist/lib/devices.js +16 -5
- package/dist/schema/field-aliases.js +95 -0
- package/dist/utils/help-json.js +54 -0
- package/dist/utils/output.js +17 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/utils/output.js
CHANGED
|
@@ -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.
|
|
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",
|