@switchbot/openapi-cli 3.1.1 → 3.2.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 (113) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +56945 -169
  3. package/dist/policy/schema/v0.2.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/api/client.js +0 -235
  6. package/dist/auth.js +0 -20
  7. package/dist/commands/agent-bootstrap.js +0 -182
  8. package/dist/commands/auth.js +0 -354
  9. package/dist/commands/batch.js +0 -413
  10. package/dist/commands/cache.js +0 -126
  11. package/dist/commands/capabilities.js +0 -385
  12. package/dist/commands/catalog.js +0 -359
  13. package/dist/commands/completion.js +0 -385
  14. package/dist/commands/config.js +0 -376
  15. package/dist/commands/daemon.js +0 -410
  16. package/dist/commands/device-meta.js +0 -159
  17. package/dist/commands/devices.js +0 -948
  18. package/dist/commands/doctor.js +0 -1015
  19. package/dist/commands/events.js +0 -563
  20. package/dist/commands/expand.js +0 -130
  21. package/dist/commands/explain.js +0 -139
  22. package/dist/commands/health.js +0 -113
  23. package/dist/commands/history.js +0 -320
  24. package/dist/commands/identity.js +0 -59
  25. package/dist/commands/install.js +0 -246
  26. package/dist/commands/mcp.js +0 -2017
  27. package/dist/commands/plan.js +0 -653
  28. package/dist/commands/policy.js +0 -586
  29. package/dist/commands/quota.js +0 -78
  30. package/dist/commands/rules.js +0 -875
  31. package/dist/commands/scenes.js +0 -264
  32. package/dist/commands/schema.js +0 -177
  33. package/dist/commands/status-sync.js +0 -131
  34. package/dist/commands/uninstall.js +0 -237
  35. package/dist/commands/upgrade-check.js +0 -107
  36. package/dist/commands/watch.js +0 -194
  37. package/dist/commands/webhook.js +0 -182
  38. package/dist/config.js +0 -258
  39. package/dist/credentials/backends/file.js +0 -101
  40. package/dist/credentials/backends/linux.js +0 -129
  41. package/dist/credentials/backends/macos.js +0 -129
  42. package/dist/credentials/backends/windows.js +0 -215
  43. package/dist/credentials/keychain.js +0 -88
  44. package/dist/credentials/prime.js +0 -52
  45. package/dist/devices/cache.js +0 -293
  46. package/dist/devices/catalog.js +0 -767
  47. package/dist/devices/device-meta.js +0 -56
  48. package/dist/devices/history-agg.js +0 -138
  49. package/dist/devices/history-query.js +0 -181
  50. package/dist/devices/param-validator.js +0 -433
  51. package/dist/devices/resources.js +0 -270
  52. package/dist/install/default-steps.js +0 -257
  53. package/dist/install/preflight.js +0 -212
  54. package/dist/install/steps.js +0 -67
  55. package/dist/lib/command-keywords.js +0 -17
  56. package/dist/lib/daemon-state.js +0 -46
  57. package/dist/lib/destructive-mode.js +0 -12
  58. package/dist/lib/devices.js +0 -382
  59. package/dist/lib/idempotency.js +0 -106
  60. package/dist/lib/plan-store.js +0 -68
  61. package/dist/lib/request-context.js +0 -12
  62. package/dist/lib/scenes.js +0 -10
  63. package/dist/logger.js +0 -16
  64. package/dist/mcp/device-history.js +0 -145
  65. package/dist/mcp/events-subscription.js +0 -213
  66. package/dist/mqtt/client.js +0 -180
  67. package/dist/mqtt/credential.js +0 -30
  68. package/dist/policy/add-rule.js +0 -124
  69. package/dist/policy/diff.js +0 -91
  70. package/dist/policy/format.js +0 -57
  71. package/dist/policy/load.js +0 -61
  72. package/dist/policy/migrate.js +0 -67
  73. package/dist/policy/schema.js +0 -18
  74. package/dist/policy/validate.js +0 -262
  75. package/dist/rules/action.js +0 -216
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -214
  78. package/dist/rules/cron-scheduler.js +0 -186
  79. package/dist/rules/destructive.js +0 -52
  80. package/dist/rules/engine.js +0 -757
  81. package/dist/rules/matcher.js +0 -230
  82. package/dist/rules/pid-file.js +0 -95
  83. package/dist/rules/quiet-hours.js +0 -45
  84. package/dist/rules/suggest.js +0 -95
  85. package/dist/rules/throttle.js +0 -116
  86. package/dist/rules/types.js +0 -34
  87. package/dist/rules/webhook-listener.js +0 -223
  88. package/dist/rules/webhook-token.js +0 -90
  89. package/dist/schema/field-aliases.js +0 -131
  90. package/dist/sinks/dispatcher.js +0 -12
  91. package/dist/sinks/file.js +0 -19
  92. package/dist/sinks/format.js +0 -56
  93. package/dist/sinks/homeassistant.js +0 -44
  94. package/dist/sinks/openclaw.js +0 -33
  95. package/dist/sinks/stdout.js +0 -5
  96. package/dist/sinks/telegram.js +0 -28
  97. package/dist/sinks/types.js +0 -1
  98. package/dist/sinks/webhook.js +0 -22
  99. package/dist/status-sync/manager.js +0 -268
  100. package/dist/utils/arg-parsers.js +0 -66
  101. package/dist/utils/audit.js +0 -121
  102. package/dist/utils/filter.js +0 -189
  103. package/dist/utils/flags.js +0 -186
  104. package/dist/utils/format.js +0 -117
  105. package/dist/utils/health.js +0 -101
  106. package/dist/utils/help-json.js +0 -54
  107. package/dist/utils/name-resolver.js +0 -137
  108. package/dist/utils/output.js +0 -404
  109. package/dist/utils/quota.js +0 -227
  110. package/dist/utils/redact.js +0 -68
  111. package/dist/utils/retry.js +0 -140
  112. package/dist/utils/string.js +0 -22
  113. package/dist/version.js +0 -4
@@ -1,130 +0,0 @@
1
- import { intArg, stringArg } from '../utils/arg-parsers.js';
2
- import { handleError, isJsonMode, printJson, UsageError, exitWithError } from '../utils/output.js';
3
- import { getCachedDevice } from '../devices/cache.js';
4
- import { executeCommand, isDestructiveCommand, getDestructiveReason } from '../lib/devices.js';
5
- import { isDryRun } from '../utils/flags.js';
6
- import { resolveDeviceId } from '../utils/name-resolver.js';
7
- import { DryRunSignal } from '../api/client.js';
8
- import { buildAcSetAll, buildCurtainSetPosition, buildBlindTiltSetPosition, buildRelaySetMode, } from '../devices/param-validator.js';
9
- // ---- Registration ----------------------------------------------------------
10
- export function registerExpandCommand(devices) {
11
- devices
12
- .command('expand')
13
- .description('Send a command with semantic flags instead of raw positional parameters')
14
- .argument('[deviceId]', 'Target device ID from "devices list" (or use --name)')
15
- .argument('[command]', 'Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2)')
16
- .option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
17
- .option('--temp <celsius>', 'AC setAll: temperature in Celsius (16-30)', intArg('--temp', { min: 16, max: 30 }))
18
- .option('--mode <mode>', 'AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary', stringArg('--mode'))
19
- .option('--fan <speed>', 'AC setAll: fan speed auto|low|mid|high', stringArg('--fan'))
20
- .option('--power <state>', 'AC setAll: on|off', stringArg('--power'))
21
- .option('--position <percent>', 'Curtain setPosition: 0-100 (0=open, 100=closed)', intArg('--position', { min: 0, max: 100 }))
22
- .option('--direction <dir>', 'Blind Tilt setPosition: up|down', stringArg('--direction'))
23
- .option('--angle <percent>', 'Blind Tilt setPosition: 0-100 (0=closed, 100=open)', intArg('--angle', { min: 0, max: 100 }))
24
- .option('--channel <n>', 'Relay Switch 2 setMode: channel 1 or 2', intArg('--channel', { min: 1, max: 2 }))
25
- .option('--yes', 'Confirm destructive commands')
26
- .addHelpText('after', `
27
- Translates semantic flags into the wire parameter format, then sends the command.
28
-
29
- Supported expansions:
30
-
31
- Air Conditioner — setAll
32
- --temp 26 --mode cool --fan low --power on → "26,2,2,on"
33
- --mode values: auto | cool | dry | fan | heat
34
- --fan values: auto | low | mid | high
35
-
36
- Curtain / Curtain 3 — setPosition
37
- --position 50 [--mode silent] → "0,1,50"
38
- --mode values: default (ff) | performance (0) | silent (1)
39
-
40
- Blind Tilt — setPosition
41
- --direction up --angle 50 → "up;50"
42
-
43
- Relay Switch 2PM — setMode
44
- --channel 1 --mode edge → "1;1"
45
- --mode values: toggle (0) | edge (1) | detached (2) | momentary (3)
46
-
47
- Examples:
48
- $ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
49
- $ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
50
- $ switchbot devices expand <blindId> setPosition --direction up --angle 50
51
- $ switchbot devices expand <relayId> setMode --channel 1 --mode edge
52
- $ switchbot devices expand <acId> setAll --temp 22 --mode heat --fan auto --power on --dry-run
53
- $ switchbot devices expand --name "Living Room AC" setAll --temp 26 --mode cool --fan low --power on
54
- `)
55
- .action(async (deviceIdArg, commandArg, options) => {
56
- let deviceId = '';
57
- let command = '';
58
- try {
59
- // When --name is provided, Commander assigns the first positional to deviceIdArg
60
- // and leaves commandArg undefined. Detect and shift.
61
- let effectiveDeviceIdArg = deviceIdArg;
62
- let effectiveCommand = commandArg;
63
- if (options.name && deviceIdArg && !commandArg) {
64
- effectiveCommand = deviceIdArg;
65
- effectiveDeviceIdArg = undefined;
66
- }
67
- deviceId = resolveDeviceId(effectiveDeviceIdArg, options.name);
68
- if (!effectiveCommand)
69
- throw new UsageError('A command argument is required (setAll, setPosition, setMode).');
70
- command = effectiveCommand;
71
- const cached = getCachedDevice(deviceId);
72
- const deviceType = cached?.type ?? '';
73
- let parameter;
74
- if (command === 'setAll') {
75
- parameter = buildAcSetAll(options);
76
- }
77
- else if (command === 'setPosition') {
78
- if (!cached) {
79
- throw new UsageError(`Device ${deviceId} is not in the local cache — run 'switchbot devices list' first so 'expand' knows whether this is a Curtain or a Blind Tilt.`);
80
- }
81
- const isBlind = deviceType.startsWith('Blind Tilt');
82
- parameter = isBlind
83
- ? buildBlindTiltSetPosition(options)
84
- : buildCurtainSetPosition(options);
85
- }
86
- else if (command === 'setMode' && deviceType.startsWith('Relay Switch')) {
87
- parameter = buildRelaySetMode(options);
88
- }
89
- else {
90
- throw new UsageError(`'expand' does not support "${command}" for device type "${deviceType || 'unknown'}". ` +
91
- `Use 'switchbot devices command' to send raw parameters instead.`);
92
- }
93
- if (!options.yes && !isDryRun() && isDestructiveCommand(deviceType, command, 'command')) {
94
- const reason = getDestructiveReason(deviceType, command, 'command');
95
- exitWithError({
96
- code: 2,
97
- kind: 'guard',
98
- message: `"${command}" on ${deviceType || 'device'} is destructive and requires --yes.`,
99
- hint: reason ? `Re-run with --yes. Reason: ${reason}` : 'Re-run with --yes to confirm.',
100
- });
101
- }
102
- const body = await executeCommand(deviceId, command, parameter, 'command');
103
- const isIr = cached?.category === 'ir';
104
- if (isJsonMode()) {
105
- const result = { ok: true, command, deviceId, parameter };
106
- if (isIr)
107
- result.subKind = 'ir-no-feedback';
108
- if (body && typeof body === 'object' && Object.keys(body).length > 0)
109
- result.response = body;
110
- printJson(result);
111
- return;
112
- }
113
- console.log(`✓ Command sent: ${command} (${parameter})`);
114
- if (isIr)
115
- console.log(' Note: IR command sent — no device confirmation (fire-and-forget).');
116
- }
117
- catch (error) {
118
- if (error instanceof DryRunSignal) {
119
- if (isJsonMode()) {
120
- printJson({ ok: true, dryRun: true, command, deviceId });
121
- }
122
- else {
123
- console.log(`◦ dry-run: ${command} would be sent to ${deviceId}`);
124
- }
125
- return;
126
- }
127
- handleError(error);
128
- }
129
- });
130
- }
@@ -1,139 +0,0 @@
1
- import { printJson, isJsonMode, handleError } from '../utils/output.js';
2
- import { describeDevice, fetchDeviceList, } from '../lib/devices.js';
3
- function deviceName(d) {
4
- return d.deviceName;
5
- }
6
- export function registerExplainCommand(devices) {
7
- devices
8
- .command('explain')
9
- .description('One-shot device summary: metadata + capabilities + live status + children (for Hubs)')
10
- .argument('<deviceId>', 'Device ID to explain')
11
- .option('--no-live', 'Skip the live status API call (catalog-only output)')
12
- .addHelpText('after', `
13
- 'explain' is the agent-friendly sibling of 'describe'. It combines:
14
- - metadata (id, name, type, category, role)
15
- - live status (unless --no-live)
16
- - commands with idempotent/destructive flags
17
- - children (for Hub devices: IR remotes bound to this hub)
18
- - suggested actions (pre-baked common usages)
19
- - warnings (deprecated types, missing cloud service, etc.)
20
-
21
- Examples:
22
- $ switchbot devices explain <id>
23
- $ switchbot --json devices explain <id> | jq '.commands[] | select(.destructive)'
24
- $ switchbot devices explain <id> --no-live
25
- `)
26
- .action(async (deviceId, options) => {
27
- try {
28
- const wantLive = options.live !== false;
29
- const desc = await describeDevice(deviceId, { live: wantLive });
30
- const warnings = [];
31
- if (desc.isPhysical && !desc.device.enableCloudService) {
32
- warnings.push('Cloud service disabled on this device — commands will fail.');
33
- }
34
- if (!desc.catalog) {
35
- warnings.push(`No catalog entry for type "${desc.typeName}". Commands cannot be validated offline.`);
36
- }
37
- let children = [];
38
- if (desc.catalog?.role === 'hub') {
39
- const body = await fetchDeviceList();
40
- children = body.infraredRemoteList
41
- .filter((ir) => ir.hubDeviceId === deviceId)
42
- .map((ir) => ({ deviceId: ir.deviceId, name: ir.deviceName, type: ir.remoteType }));
43
- }
44
- const caps = desc.capabilities;
45
- const commands = caps && 'commands' in caps
46
- ? caps.commands.map((c) => {
47
- const tier = c.safetyTier;
48
- return {
49
- command: c.command,
50
- parameter: c.parameter,
51
- idempotent: c.idempotent,
52
- ...(tier ? { safetyTier: tier } : {}),
53
- };
54
- })
55
- : [];
56
- const statusFields = caps && 'statusFields' in caps ? caps.statusFields : [];
57
- const liveStatus = caps && 'liveStatus' in caps ? caps.liveStatus : undefined;
58
- const location = desc.isPhysical
59
- ? {
60
- family: desc.device.familyName,
61
- room: desc.device.roomName ?? undefined,
62
- }
63
- : desc.inheritedLocation
64
- ? { family: desc.inheritedLocation.family, room: desc.inheritedLocation.room }
65
- : undefined;
66
- const result = {
67
- deviceId,
68
- type: desc.typeName,
69
- category: desc.isPhysical ? 'physical' : 'ir',
70
- name: deviceName(desc.device),
71
- role: desc.catalog?.role ?? null,
72
- readOnly: desc.catalog?.readOnly ?? false,
73
- location,
74
- liveStatus,
75
- commands,
76
- statusFields,
77
- children,
78
- suggestedActions: desc.suggestedActions,
79
- warnings,
80
- };
81
- if (isJsonMode()) {
82
- printJson(result);
83
- return;
84
- }
85
- printHuman(result);
86
- }
87
- catch (err) {
88
- handleError(err);
89
- }
90
- });
91
- }
92
- function printHuman(r) {
93
- console.log(`# ${r.name} (${r.deviceId})`);
94
- console.log(`type: ${r.type} [${r.category}${r.role ? ', ' + r.role : ''}${r.readOnly ? ', read-only' : ''}]`);
95
- if (r.location?.family || r.location?.room) {
96
- const loc = [r.location?.family, r.location?.room].filter(Boolean).join(' / ');
97
- console.log(`location: ${loc}`);
98
- }
99
- if (r.warnings.length) {
100
- console.log('warnings:');
101
- for (const w of r.warnings)
102
- console.log(` ! ${w}`);
103
- }
104
- if (r.liveStatus && !('error' in r.liveStatus)) {
105
- console.log('live status:');
106
- for (const [k, v] of Object.entries(r.liveStatus)) {
107
- console.log(` ${k}: ${JSON.stringify(v)}`);
108
- }
109
- }
110
- else if (r.liveStatus && 'error' in r.liveStatus) {
111
- console.log(`live status: error — ${r.liveStatus.error}`);
112
- }
113
- if (r.commands.length) {
114
- console.log('commands:');
115
- for (const c of r.commands) {
116
- const flags = [c.idempotent && 'idempotent', c.safetyTier === 'destructive' && 'destructive']
117
- .filter(Boolean)
118
- .join(', ');
119
- const suffix = flags ? ` [${flags}]` : '';
120
- console.log(` ${c.command}${c.parameter !== '—' ? ` <${c.parameter}>` : ''}${suffix}`);
121
- }
122
- }
123
- if (r.statusFields.length) {
124
- console.log(`status fields: ${r.statusFields.join(', ')}`);
125
- }
126
- if (r.children.length) {
127
- console.log(`children (${r.children.length}):`);
128
- for (const c of r.children) {
129
- console.log(` ${c.deviceId} ${c.name} [${c.type}]`);
130
- }
131
- }
132
- if (r.suggestedActions.length) {
133
- console.log('suggested:');
134
- for (const s of r.suggestedActions) {
135
- const param = s.parameter ? ` ${s.parameter}` : '';
136
- console.log(` ${s.description}: ${s.command}${param}`);
137
- }
138
- }
139
- }
@@ -1,113 +0,0 @@
1
- import http from 'node:http';
2
- import { printJson, isJsonMode, printTable, handleError } from '../utils/output.js';
3
- import { getHealthReport, toPrometheusText } from '../utils/health.js';
4
- import { intArg } from '../utils/arg-parsers.js';
5
- const HEALTHZ_SCHEMA_VERSION = '1.1';
6
- /**
7
- * Create an HTTP request handler for the health endpoints. Exposed separately
8
- * so integration tests can call it directly without binding a port.
9
- */
10
- export function createHealthHandler(auditLogPath) {
11
- return (req, res) => {
12
- const url = (req.url ?? '/').split('?')[0];
13
- if (url === '/healthz') {
14
- const report = getHealthReport(auditLogPath);
15
- const statusCode = report.overall === 'down' ? 503 : 200;
16
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
17
- res.end(JSON.stringify({ schemaVersion: HEALTHZ_SCHEMA_VERSION, data: report }));
18
- }
19
- else if (url === '/metrics') {
20
- const report = getHealthReport(auditLogPath);
21
- res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8' });
22
- res.end(toPrometheusText(report));
23
- }
24
- else {
25
- res.writeHead(404, { 'Content-Type': 'application/json' });
26
- res.end(JSON.stringify({ error: 'Not found', paths: ['/healthz', '/metrics'] }));
27
- }
28
- };
29
- }
30
- export function registerHealthCommand(program) {
31
- const health = program
32
- .command('health')
33
- .description('Report process health: quota, audit error rate, circuit breaker state.');
34
- health
35
- .command('check')
36
- .description('Print a one-shot health report.')
37
- .option('--prometheus', 'Emit Prometheus text format.')
38
- .option('--audit-log <path>', 'Audit log path (default: ~/.switchbot/audit.log).')
39
- .action((opts) => {
40
- const report = getHealthReport(opts.auditLog);
41
- if (opts.prometheus) {
42
- process.stdout.write(toPrometheusText(report));
43
- return;
44
- }
45
- if (isJsonMode()) {
46
- printJson(report);
47
- return;
48
- }
49
- const statusEmoji = report.overall === 'ok' ? '✓' : report.overall === 'degraded' ? '⚠' : '✗';
50
- console.log(`${statusEmoji} overall: ${report.overall} (${report.generatedAt})`);
51
- console.log('');
52
- printTable(['Component', 'Status', 'Detail'], [
53
- ['quota', report.quota.status,
54
- `${report.quota.used}/${report.quota.limit} (${report.quota.percentUsed}% used, ${report.quota.remaining} remaining)`],
55
- ['audit', report.audit.status,
56
- report.audit.present
57
- ? `${report.audit.recentErrors}/${report.audit.recentTotal} errors in 24h (${report.audit.errorRatePercent}%)`
58
- : 'log not present'],
59
- ['circuit', report.circuit.status,
60
- `${report.circuit.name}: ${report.circuit.state} (failures: ${report.circuit.failures})`],
61
- ['process', 'ok',
62
- `pid ${report.process.pid} · uptime ${report.process.uptimeSeconds}s · mem ${report.process.memoryMb}MB`],
63
- ]);
64
- if (report.overall !== 'ok')
65
- process.exit(1);
66
- });
67
- // switchbot health serve [--port <n>]
68
- health
69
- .command('serve')
70
- .description('Start an HTTP server exposing /healthz (JSON) and /metrics (Prometheus).')
71
- .option('--port <n>', 'Port to listen on.', intArg('--port'), '3100')
72
- .option('--host <host>', 'Bind address.', '127.0.0.1')
73
- .option('--audit-log <path>', 'Audit log path.')
74
- .addHelpText('after', `
75
- Endpoints:
76
- GET /healthz JSON health report (HTTP 200 ok/degraded, 503 when circuit is open).
77
- GET /metrics Prometheus text metrics.
78
-
79
- Example:
80
- $ switchbot health serve --port 3100
81
- $ curl http://127.0.0.1:3100/healthz
82
- `)
83
- .action((opts) => {
84
- const port = parseInt(opts.port, 10);
85
- const handler = createHealthHandler(opts.auditLog);
86
- const server = http.createServer(handler);
87
- server.on('error', (err) => {
88
- if (err.code === 'EADDRINUSE') {
89
- handleError(Object.assign(new Error(`Port ${port} is already in use. Choose a different port with --port.`), { code: err.code }));
90
- }
91
- else {
92
- handleError(err);
93
- }
94
- });
95
- server.listen(port, opts.host, () => {
96
- const addr = server.address();
97
- const boundPort = typeof addr === 'object' && addr !== null ? addr.port : port;
98
- if (isJsonMode()) {
99
- printJson({ status: 'listening', host: opts.host, port: boundPort, endpoints: ['/healthz', '/metrics'] });
100
- }
101
- else {
102
- console.log(`health server listening on ${opts.host}:${boundPort}`);
103
- console.log(' GET /healthz — JSON health report');
104
- console.log(' GET /metrics — Prometheus text metrics');
105
- }
106
- });
107
- function shutdown() {
108
- server.close(() => process.exit(0));
109
- }
110
- process.on('SIGTERM', shutdown);
111
- process.on('SIGINT', shutdown);
112
- });
113
- }