@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.
- package/README.md +3 -3
- package/dist/index.js +56945 -169
- package/dist/policy/schema/v0.2.json +1 -1
- package/package.json +3 -2
- package/dist/api/client.js +0 -235
- package/dist/auth.js +0 -20
- package/dist/commands/agent-bootstrap.js +0 -182
- package/dist/commands/auth.js +0 -354
- package/dist/commands/batch.js +0 -413
- package/dist/commands/cache.js +0 -126
- package/dist/commands/capabilities.js +0 -385
- package/dist/commands/catalog.js +0 -359
- package/dist/commands/completion.js +0 -385
- package/dist/commands/config.js +0 -376
- package/dist/commands/daemon.js +0 -410
- package/dist/commands/device-meta.js +0 -159
- package/dist/commands/devices.js +0 -948
- package/dist/commands/doctor.js +0 -1015
- package/dist/commands/events.js +0 -563
- package/dist/commands/expand.js +0 -130
- package/dist/commands/explain.js +0 -139
- package/dist/commands/health.js +0 -113
- package/dist/commands/history.js +0 -320
- package/dist/commands/identity.js +0 -59
- package/dist/commands/install.js +0 -246
- package/dist/commands/mcp.js +0 -2017
- package/dist/commands/plan.js +0 -653
- package/dist/commands/policy.js +0 -586
- package/dist/commands/quota.js +0 -78
- package/dist/commands/rules.js +0 -875
- package/dist/commands/scenes.js +0 -264
- package/dist/commands/schema.js +0 -177
- package/dist/commands/status-sync.js +0 -131
- package/dist/commands/uninstall.js +0 -237
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/watch.js +0 -194
- package/dist/commands/webhook.js +0 -182
- package/dist/config.js +0 -258
- package/dist/credentials/backends/file.js +0 -101
- package/dist/credentials/backends/linux.js +0 -129
- package/dist/credentials/backends/macos.js +0 -129
- package/dist/credentials/backends/windows.js +0 -215
- package/dist/credentials/keychain.js +0 -88
- package/dist/credentials/prime.js +0 -52
- package/dist/devices/cache.js +0 -293
- package/dist/devices/catalog.js +0 -767
- package/dist/devices/device-meta.js +0 -56
- package/dist/devices/history-agg.js +0 -138
- package/dist/devices/history-query.js +0 -181
- package/dist/devices/param-validator.js +0 -433
- package/dist/devices/resources.js +0 -270
- package/dist/install/default-steps.js +0 -257
- package/dist/install/preflight.js +0 -212
- package/dist/install/steps.js +0 -67
- package/dist/lib/command-keywords.js +0 -17
- package/dist/lib/daemon-state.js +0 -46
- package/dist/lib/destructive-mode.js +0 -12
- package/dist/lib/devices.js +0 -382
- package/dist/lib/idempotency.js +0 -106
- package/dist/lib/plan-store.js +0 -68
- package/dist/lib/request-context.js +0 -12
- package/dist/lib/scenes.js +0 -10
- package/dist/logger.js +0 -16
- package/dist/mcp/device-history.js +0 -145
- package/dist/mcp/events-subscription.js +0 -213
- package/dist/mqtt/client.js +0 -180
- package/dist/mqtt/credential.js +0 -30
- package/dist/policy/add-rule.js +0 -124
- package/dist/policy/diff.js +0 -91
- package/dist/policy/format.js +0 -57
- package/dist/policy/load.js +0 -61
- package/dist/policy/migrate.js +0 -67
- package/dist/policy/schema.js +0 -18
- package/dist/policy/validate.js +0 -262
- package/dist/rules/action.js +0 -216
- package/dist/rules/audit-query.js +0 -89
- package/dist/rules/conflict-analyzer.js +0 -214
- package/dist/rules/cron-scheduler.js +0 -186
- package/dist/rules/destructive.js +0 -52
- package/dist/rules/engine.js +0 -757
- package/dist/rules/matcher.js +0 -230
- package/dist/rules/pid-file.js +0 -95
- package/dist/rules/quiet-hours.js +0 -45
- package/dist/rules/suggest.js +0 -95
- package/dist/rules/throttle.js +0 -116
- package/dist/rules/types.js +0 -34
- package/dist/rules/webhook-listener.js +0 -223
- package/dist/rules/webhook-token.js +0 -90
- package/dist/schema/field-aliases.js +0 -131
- package/dist/sinks/dispatcher.js +0 -12
- package/dist/sinks/file.js +0 -19
- package/dist/sinks/format.js +0 -56
- package/dist/sinks/homeassistant.js +0 -44
- package/dist/sinks/openclaw.js +0 -33
- package/dist/sinks/stdout.js +0 -5
- package/dist/sinks/telegram.js +0 -28
- package/dist/sinks/types.js +0 -1
- package/dist/sinks/webhook.js +0 -22
- package/dist/status-sync/manager.js +0 -268
- package/dist/utils/arg-parsers.js +0 -66
- package/dist/utils/audit.js +0 -121
- package/dist/utils/filter.js +0 -189
- package/dist/utils/flags.js +0 -186
- package/dist/utils/format.js +0 -117
- package/dist/utils/health.js +0 -101
- package/dist/utils/help-json.js +0 -54
- package/dist/utils/name-resolver.js +0 -137
- package/dist/utils/output.js +0 -404
- package/dist/utils/quota.js +0 -227
- package/dist/utils/redact.js +0 -68
- package/dist/utils/retry.js +0 -140
- package/dist/utils/string.js +0 -22
- package/dist/version.js +0 -4
package/dist/commands/expand.js
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/explain.js
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/health.js
DELETED
|
@@ -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
|
-
}
|