@switchbot/openapi-cli 2.6.3 → 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 +28 -35
- package/dist/commands/capabilities.js +29 -21
- package/dist/commands/catalog.js +12 -3
- package/dist/commands/config.js +32 -38
- package/dist/commands/devices.js +124 -83
- 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 +168 -73
- package/dist/commands/plan.js +1 -1
- package/dist/commands/schema.js +22 -12
- package/dist/commands/watch.js +15 -2
- package/dist/config.js +65 -21
- package/dist/devices/catalog.js +125 -12
- package/dist/devices/resources.js +270 -0
- package/dist/index.js +25 -6
- package/dist/lib/devices.js +22 -7
- package/dist/schema/field-aliases.js +131 -0
- package/dist/utils/filter.js +17 -4
- package/dist/utils/help-json.js +54 -0
- package/dist/utils/output.js +37 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,9 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { intArg, stringArg, enumArg } from './utils/arg-parsers.js';
|
|
6
6
|
import { parseDurationToMs } from './utils/flags.js';
|
|
7
|
+
import { emitJsonError, isJsonMode, printJson } from './utils/output.js';
|
|
8
|
+
import { commandToJson, resolveTargetCommand } from './utils/help-json.js';
|
|
9
|
+
import { PRODUCT_TAGLINE } from './commands/identity.js';
|
|
7
10
|
import { registerConfigCommand } from './commands/config.js';
|
|
8
11
|
import { registerDevicesCommand } from './commands/devices.js';
|
|
9
12
|
import { registerScenesCommand } from './commands/scenes.js';
|
|
@@ -28,6 +31,11 @@ if (process.argv.includes('--no-color') || Boolean(process.env.NO_COLOR)) {
|
|
|
28
31
|
chalk.level = 0;
|
|
29
32
|
}
|
|
30
33
|
const program = new Command();
|
|
34
|
+
if (isJsonMode()) {
|
|
35
|
+
// In --json mode, commander writes plain-text usage errors by default.
|
|
36
|
+
// Silence that channel and emit a single structured error in the catch block.
|
|
37
|
+
program.configureOutput({ writeErr: () => { } });
|
|
38
|
+
}
|
|
31
39
|
// Top-level subcommand names. Used by stringArg to produce clearer errors when
|
|
32
40
|
// a value is omitted and the next argv token turns out to be a subcommand name.
|
|
33
41
|
const TOP_LEVEL_COMMANDS = [
|
|
@@ -48,7 +56,7 @@ const cacheModeArg = (value) => {
|
|
|
48
56
|
};
|
|
49
57
|
program
|
|
50
58
|
.name('switchbot')
|
|
51
|
-
.description(
|
|
59
|
+
.description(PRODUCT_TAGLINE)
|
|
52
60
|
.version(pkgVersion)
|
|
53
61
|
.option('--no-color', 'Disable ANSI colors in output')
|
|
54
62
|
.option('--json', 'Output raw JSON response (disables tables; useful for pipes/scripts)')
|
|
@@ -130,10 +138,7 @@ Docs: https://github.com/OpenWonderLabs/SwitchBotAPI
|
|
|
130
138
|
// per-command: subcommand errors won't bubble to the root override, so walk
|
|
131
139
|
// every registered command and apply the same handler.
|
|
132
140
|
const usageExitHandler = (err) => {
|
|
133
|
-
|
|
134
|
-
process.exit(0);
|
|
135
|
-
}
|
|
136
|
-
process.exit(2);
|
|
141
|
+
throw err;
|
|
137
142
|
};
|
|
138
143
|
function applyExitOverride(cmd) {
|
|
139
144
|
cmd.exitOverride(usageExitHandler);
|
|
@@ -147,6 +152,10 @@ function enableSuggestions(cmd) {
|
|
|
147
152
|
cmd.commands.forEach(enableSuggestions);
|
|
148
153
|
}
|
|
149
154
|
enableSuggestions(program);
|
|
155
|
+
// In JSON mode suppress the plain-text help output so we can emit structured JSON instead.
|
|
156
|
+
if (isJsonMode()) {
|
|
157
|
+
program.configureOutput({ writeOut: () => { } });
|
|
158
|
+
}
|
|
150
159
|
try {
|
|
151
160
|
await program.parseAsync();
|
|
152
161
|
}
|
|
@@ -155,9 +164,19 @@ catch (err) {
|
|
|
155
164
|
// argParser on a subcommand option) don't always hit the root exitOverride.
|
|
156
165
|
// Mirror the root mapping so all usage errors surface as exit 2.
|
|
157
166
|
if (err instanceof CommanderError) {
|
|
158
|
-
if (err.code === 'commander.helpDisplayed'
|
|
167
|
+
if (err.code === 'commander.helpDisplayed') {
|
|
168
|
+
if (isJsonMode()) {
|
|
169
|
+
const target = resolveTargetCommand(program, process.argv.slice(2));
|
|
170
|
+
printJson(commandToJson(target, { includeIdentity: target === program }));
|
|
171
|
+
}
|
|
159
172
|
process.exit(0);
|
|
160
173
|
}
|
|
174
|
+
if (err.code === 'commander.version') {
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
if (isJsonMode()) {
|
|
178
|
+
emitJsonError({ code: 2, kind: 'usage', message: err.message });
|
|
179
|
+
}
|
|
161
180
|
process.exit(2);
|
|
162
181
|
}
|
|
163
182
|
throw err;
|
package/dist/lib/devices.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient } from '../api/client.js';
|
|
2
2
|
import { idempotencyCache } from './idempotency.js';
|
|
3
|
-
import { findCatalogEntry, suggestedActions, getEffectiveCatalog, } from '../devices/catalog.js';
|
|
3
|
+
import { findCatalogEntry, suggestedActions, getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
|
|
4
4
|
import { getCachedDevice, updateCacheFromDeviceList, loadCache, isListCacheFresh, getCachedStatus, setCachedStatus, } from '../devices/cache.js';
|
|
5
5
|
import { getCacheMode } from '../utils/flags.js';
|
|
6
6
|
import { writeAudit } from '../utils/audit.js';
|
|
@@ -151,8 +151,12 @@ export function validateCommand(deviceId, cmd, parameter, commandType) {
|
|
|
151
151
|
if (!match || Array.isArray(match))
|
|
152
152
|
return { ok: true };
|
|
153
153
|
const builtinCommands = match.commands.filter((c) => c.commandType !== 'customize');
|
|
154
|
-
if (builtinCommands.length === 0)
|
|
155
|
-
return {
|
|
154
|
+
if (match.readOnly || builtinCommands.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: new CommandValidationError(`${cached.name} (${cached.type}) is a read-only sensor — it has no control commands.`, 'read-only-device', `Use 'switchbot devices status ${deviceId}' to read this device instead.`),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
156
160
|
let spec = builtinCommands.find((c) => c.command === cmd);
|
|
157
161
|
let caseNormalizedFrom;
|
|
158
162
|
let normalizedCmd = cmd;
|
|
@@ -212,9 +216,11 @@ export function isDestructiveCommand(deviceType, cmd, commandType) {
|
|
|
212
216
|
if (!match || Array.isArray(match))
|
|
213
217
|
return false;
|
|
214
218
|
const spec = match.commands.find((c) => c.command === cmd);
|
|
215
|
-
|
|
219
|
+
if (!spec)
|
|
220
|
+
return false;
|
|
221
|
+
return deriveSafetyTier(spec, match) === 'destructive';
|
|
216
222
|
}
|
|
217
|
-
/** Return the
|
|
223
|
+
/** Return the safetyReason for a command, or null if not destructive / not found. */
|
|
218
224
|
export function getDestructiveReason(deviceType, cmd, commandType) {
|
|
219
225
|
if (commandType === 'customize')
|
|
220
226
|
return null;
|
|
@@ -224,7 +230,7 @@ export function getDestructiveReason(deviceType, cmd, commandType) {
|
|
|
224
230
|
if (!match || Array.isArray(match))
|
|
225
231
|
return null;
|
|
226
232
|
const spec = match.commands.find((c) => c.command === cmd);
|
|
227
|
-
return spec
|
|
233
|
+
return spec ? getCommandSafetyReason(spec) : null;
|
|
228
234
|
}
|
|
229
235
|
/**
|
|
230
236
|
* Describe a device by id: metadata + catalog entry (if known) +
|
|
@@ -260,7 +266,16 @@ export async function describeDevice(deviceId, options = {}, client) {
|
|
|
260
266
|
? {
|
|
261
267
|
role: catalogEntry.role ?? null,
|
|
262
268
|
readOnly: catalogEntry.readOnly ?? false,
|
|
263
|
-
commands: catalogEntry.commands
|
|
269
|
+
commands: catalogEntry.commands.map((c) => {
|
|
270
|
+
const tier = deriveSafetyTier(c, catalogEntry);
|
|
271
|
+
const reason = getCommandSafetyReason(c);
|
|
272
|
+
return {
|
|
273
|
+
...c,
|
|
274
|
+
safetyTier: tier,
|
|
275
|
+
destructive: tier === 'destructive',
|
|
276
|
+
...(reason ? { safetyReason: reason } : {}),
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
264
279
|
statusFields: catalogEntry.statusFields ?? [],
|
|
265
280
|
...(liveStatus !== undefined ? { liveStatus } : {}),
|
|
266
281
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
*/
|
|
16
|
+
export const FIELD_ALIASES = {
|
|
17
|
+
// Identification (shared with list/filter)
|
|
18
|
+
deviceId: ['id'],
|
|
19
|
+
deviceName: ['name'],
|
|
20
|
+
deviceType: ['type'],
|
|
21
|
+
controlType: ['control'],
|
|
22
|
+
roomName: ['room'],
|
|
23
|
+
roomID: ['roomid'],
|
|
24
|
+
familyName: ['family'],
|
|
25
|
+
hubDeviceId: ['hub'],
|
|
26
|
+
enableCloudService: ['cloud'],
|
|
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'],
|
|
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
|
+
*/
|
|
93
|
+
export function resolveField(input, allowedCanonical) {
|
|
94
|
+
const normalized = input.trim().toLowerCase();
|
|
95
|
+
if (!normalized) {
|
|
96
|
+
throw new UsageError('Field name cannot be empty.');
|
|
97
|
+
}
|
|
98
|
+
for (const canonical of allowedCanonical) {
|
|
99
|
+
if (canonical.toLowerCase() === normalized)
|
|
100
|
+
return canonical;
|
|
101
|
+
}
|
|
102
|
+
for (const canonical of allowedCanonical) {
|
|
103
|
+
const aliases = FIELD_ALIASES[canonical] ?? [];
|
|
104
|
+
if (aliases.some((a) => a.toLowerCase() === normalized))
|
|
105
|
+
return canonical;
|
|
106
|
+
}
|
|
107
|
+
throw new UsageError(`Unknown field "${input}". Supported: ${listSupportedFieldInputs(allowedCanonical).join(', ')}`);
|
|
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
|
+
}
|
|
116
|
+
export function listSupportedFieldInputs(allowedCanonical) {
|
|
117
|
+
const out = new Set();
|
|
118
|
+
for (const canonical of allowedCanonical) {
|
|
119
|
+
out.add(canonical);
|
|
120
|
+
for (const alias of FIELD_ALIASES[canonical] ?? [])
|
|
121
|
+
out.add(alias);
|
|
122
|
+
}
|
|
123
|
+
return [...out];
|
|
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
|
+
}
|
package/dist/utils/filter.js
CHANGED
|
@@ -20,7 +20,7 @@ export class FilterSyntaxError extends Error {
|
|
|
20
20
|
* {type,name,category,room}; `devices batch` uses {type,family,room,category};
|
|
21
21
|
* `events tail` uses {deviceId,type}.
|
|
22
22
|
*/
|
|
23
|
-
export function parseFilterExpr(expr, allowedKeys) {
|
|
23
|
+
export function parseFilterExpr(expr, allowedKeys, options) {
|
|
24
24
|
if (!expr)
|
|
25
25
|
return [];
|
|
26
26
|
const parts = expr.split(',').map((p) => p.trim()).filter((p) => p.length > 0);
|
|
@@ -72,10 +72,23 @@ export function parseFilterExpr(expr, allowedKeys) {
|
|
|
72
72
|
if (!raw) {
|
|
73
73
|
throw new FilterSyntaxError(`Empty value for filter clause "${part}"`);
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
let resolvedKey = key;
|
|
76
|
+
if (options?.resolveKey) {
|
|
77
|
+
try {
|
|
78
|
+
resolvedKey = options.resolveKey(key);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (err instanceof Error) {
|
|
82
|
+
throw new FilterSyntaxError(err.message);
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!allowedKeys.includes(resolvedKey)) {
|
|
88
|
+
const printableKeys = options?.supportedKeys ?? allowedKeys;
|
|
89
|
+
throw new FilterSyntaxError(`Unknown filter key "${key}" – supported: ${printableKeys.join(', ')}`);
|
|
77
90
|
}
|
|
78
|
-
clauses.push({ key, op, raw, regex });
|
|
91
|
+
clauses.push({ key: resolvedKey, op, raw, regex });
|
|
79
92
|
}
|
|
80
93
|
return clauses;
|
|
81
94
|
}
|
|
@@ -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,43 @@ 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
|
+
}
|
|
48
|
+
export function exitWithError(messageOrOpts) {
|
|
49
|
+
const opts = typeof messageOrOpts === 'string' ? { message: messageOrOpts } : messageOrOpts;
|
|
50
|
+
const { message, kind = 'usage', code = 2, hint, context, extra } = opts;
|
|
51
|
+
if (isJsonMode()) {
|
|
52
|
+
const payload = { code, kind, message };
|
|
53
|
+
if (hint)
|
|
54
|
+
payload.hint = hint;
|
|
55
|
+
if (context)
|
|
56
|
+
payload.context = context;
|
|
57
|
+
if (extra)
|
|
58
|
+
Object.assign(payload, extra);
|
|
59
|
+
emitJsonError(payload);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.error(message);
|
|
63
|
+
if (hint)
|
|
64
|
+
console.error(hint);
|
|
65
|
+
}
|
|
66
|
+
process.exit(code);
|
|
67
|
+
}
|
|
31
68
|
function escapeMarkdownCell(s) {
|
|
32
69
|
// Pipes break markdown table layout; backslash-escape them. Collapse
|
|
33
70
|
// newlines into <br> so each row stays on one line.
|
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",
|