@switchbot/openapi-cli 3.2.1 → 3.2.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 +3 -1
- package/dist/index.js +57349 -170
- package/package.json +9 -5
- package/dist/api/client.d.ts +0 -31
- package/dist/api/client.js +0 -236
- package/dist/api/client.js.map +0 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js +0 -21
- package/dist/auth.js.map +0 -1
- package/dist/commands/agent-bootstrap.d.ts +0 -10
- package/dist/commands/agent-bootstrap.js +0 -200
- package/dist/commands/agent-bootstrap.js.map +0 -1
- package/dist/commands/auth.d.ts +0 -18
- package/dist/commands/auth.js +0 -355
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -2
- package/dist/commands/batch.js +0 -414
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/cache.d.ts +0 -2
- package/dist/commands/cache.js +0 -127
- package/dist/commands/cache.js.map +0 -1
- package/dist/commands/capabilities.d.ts +0 -31
- package/dist/commands/capabilities.js +0 -383
- package/dist/commands/capabilities.js.map +0 -1
- package/dist/commands/catalog.d.ts +0 -2
- package/dist/commands/catalog.js +0 -360
- package/dist/commands/catalog.js.map +0 -1
- package/dist/commands/completion.d.ts +0 -2
- package/dist/commands/completion.js +0 -386
- package/dist/commands/completion.js.map +0 -1
- package/dist/commands/config.d.ts +0 -21
- package/dist/commands/config.js +0 -377
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/daemon.d.ts +0 -2
- package/dist/commands/daemon.js +0 -411
- package/dist/commands/daemon.js.map +0 -1
- package/dist/commands/device-meta.d.ts +0 -2
- package/dist/commands/device-meta.js +0 -160
- package/dist/commands/device-meta.js.map +0 -1
- package/dist/commands/devices.d.ts +0 -2
- package/dist/commands/devices.js +0 -949
- package/dist/commands/devices.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -3
- package/dist/commands/doctor.js +0 -1016
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/events.d.ts +0 -31
- package/dist/commands/events.js +0 -564
- package/dist/commands/events.js.map +0 -1
- package/dist/commands/expand.d.ts +0 -2
- package/dist/commands/expand.js +0 -131
- package/dist/commands/expand.js.map +0 -1
- package/dist/commands/explain.d.ts +0 -2
- package/dist/commands/explain.js +0 -140
- package/dist/commands/explain.js.map +0 -1
- package/dist/commands/health.d.ts +0 -8
- package/dist/commands/health.js +0 -114
- package/dist/commands/health.js.map +0 -1
- package/dist/commands/history.d.ts +0 -2
- package/dist/commands/history.js +0 -321
- package/dist/commands/history.js.map +0 -1
- package/dist/commands/identity.d.ts +0 -45
- package/dist/commands/identity.js +0 -60
- package/dist/commands/identity.js.map +0 -1
- package/dist/commands/install.d.ts +0 -20
- package/dist/commands/install.js +0 -247
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/mcp.d.ts +0 -14
- package/dist/commands/mcp.js +0 -2018
- package/dist/commands/mcp.js.map +0 -1
- package/dist/commands/plan.d.ts +0 -51
- package/dist/commands/plan.js +0 -654
- package/dist/commands/plan.js.map +0 -1
- package/dist/commands/policy.d.ts +0 -24
- package/dist/commands/policy.js +0 -587
- package/dist/commands/policy.js.map +0 -1
- package/dist/commands/quota.d.ts +0 -2
- package/dist/commands/quota.js +0 -79
- package/dist/commands/quota.js.map +0 -1
- package/dist/commands/rules.d.ts +0 -2
- package/dist/commands/rules.js +0 -876
- package/dist/commands/rules.js.map +0 -1
- package/dist/commands/scenes.d.ts +0 -2
- package/dist/commands/scenes.js +0 -265
- package/dist/commands/scenes.js.map +0 -1
- package/dist/commands/schema.d.ts +0 -2
- package/dist/commands/schema.js +0 -185
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/status-sync.d.ts +0 -2
- package/dist/commands/status-sync.js +0 -132
- package/dist/commands/status-sync.js.map +0 -1
- package/dist/commands/uninstall.d.ts +0 -20
- package/dist/commands/uninstall.js +0 -238
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/upgrade-check.d.ts +0 -2
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/upgrade-check.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -2
- package/dist/commands/watch.js +0 -195
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/webhook.d.ts +0 -2
- package/dist/commands/webhook.js +0 -183
- package/dist/commands/webhook.js.map +0 -1
- package/dist/config.d.ts +0 -57
- package/dist/config.js +0 -259
- package/dist/config.js.map +0 -1
- package/dist/credentials/backends/file.d.ts +0 -18
- package/dist/credentials/backends/file.js +0 -102
- package/dist/credentials/backends/file.js.map +0 -1
- package/dist/credentials/backends/linux.d.ts +0 -16
- package/dist/credentials/backends/linux.js +0 -130
- package/dist/credentials/backends/linux.js.map +0 -1
- package/dist/credentials/backends/macos.d.ts +0 -18
- package/dist/credentials/backends/macos.js +0 -130
- package/dist/credentials/backends/macos.js.map +0 -1
- package/dist/credentials/backends/windows.d.ts +0 -23
- package/dist/credentials/backends/windows.js +0 -216
- package/dist/credentials/backends/windows.js.map +0 -1
- package/dist/credentials/keychain.d.ts +0 -83
- package/dist/credentials/keychain.js +0 -89
- package/dist/credentials/keychain.js.map +0 -1
- package/dist/credentials/prime.d.ts +0 -32
- package/dist/credentials/prime.js +0 -53
- package/dist/credentials/prime.js.map +0 -1
- package/dist/devices/cache.d.ts +0 -79
- package/dist/devices/cache.js +0 -294
- package/dist/devices/cache.js.map +0 -1
- package/dist/devices/catalog.d.ts +0 -138
- package/dist/devices/catalog.js +0 -768
- package/dist/devices/catalog.js.map +0 -1
- package/dist/devices/device-meta.d.ts +0 -15
- package/dist/devices/device-meta.js +0 -57
- package/dist/devices/device-meta.js.map +0 -1
- package/dist/devices/history-agg.d.ts +0 -37
- package/dist/devices/history-agg.js +0 -139
- package/dist/devices/history-agg.js.map +0 -1
- package/dist/devices/history-query.d.ts +0 -45
- package/dist/devices/history-query.js +0 -182
- package/dist/devices/history-query.js.map +0 -1
- package/dist/devices/param-validator.d.ts +0 -40
- package/dist/devices/param-validator.js +0 -434
- package/dist/devices/param-validator.js.map +0 -1
- package/dist/devices/resources.d.ts +0 -74
- package/dist/devices/resources.js +0 -271
- package/dist/devices/resources.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js.map +0 -1
- package/dist/install/default-steps.d.ts +0 -66
- package/dist/install/default-steps.js +0 -258
- package/dist/install/default-steps.js.map +0 -1
- package/dist/install/preflight.d.ts +0 -60
- package/dist/install/preflight.js +0 -213
- package/dist/install/preflight.js.map +0 -1
- package/dist/install/steps.d.ts +0 -61
- package/dist/install/steps.js +0 -68
- package/dist/install/steps.js.map +0 -1
- package/dist/lib/command-keywords.d.ts +0 -5
- package/dist/lib/command-keywords.js +0 -18
- package/dist/lib/command-keywords.js.map +0 -1
- package/dist/lib/daemon-state.d.ts +0 -24
- package/dist/lib/daemon-state.js +0 -47
- package/dist/lib/daemon-state.js.map +0 -1
- package/dist/lib/destructive-mode.d.ts +0 -2
- package/dist/lib/destructive-mode.js +0 -13
- package/dist/lib/destructive-mode.js.map +0 -1
- package/dist/lib/devices.d.ts +0 -151
- package/dist/lib/devices.js +0 -383
- package/dist/lib/devices.js.map +0 -1
- package/dist/lib/idempotency.d.ts +0 -46
- package/dist/lib/idempotency.js +0 -107
- package/dist/lib/idempotency.js.map +0 -1
- package/dist/lib/plan-store.d.ts +0 -19
- package/dist/lib/plan-store.js +0 -69
- package/dist/lib/plan-store.js.map +0 -1
- package/dist/lib/request-context.d.ts +0 -7
- package/dist/lib/request-context.js +0 -13
- package/dist/lib/request-context.js.map +0 -1
- package/dist/lib/scenes.d.ts +0 -7
- package/dist/lib/scenes.js +0 -11
- package/dist/lib/scenes.js.map +0 -1
- package/dist/logger.d.ts +0 -4
- package/dist/logger.js +0 -17
- package/dist/logger.js.map +0 -1
- package/dist/mcp/device-history.d.ts +0 -36
- package/dist/mcp/device-history.js +0 -146
- package/dist/mcp/device-history.js.map +0 -1
- package/dist/mcp/events-subscription.d.ts +0 -45
- package/dist/mcp/events-subscription.js +0 -214
- package/dist/mcp/events-subscription.js.map +0 -1
- package/dist/mqtt/client.d.ts +0 -25
- package/dist/mqtt/client.js +0 -181
- package/dist/mqtt/client.js.map +0 -1
- package/dist/mqtt/credential.d.ts +0 -16
- package/dist/mqtt/credential.js +0 -31
- package/dist/mqtt/credential.js.map +0 -1
- package/dist/policy/add-rule.d.ts +0 -21
- package/dist/policy/add-rule.js +0 -125
- package/dist/policy/add-rule.js.map +0 -1
- package/dist/policy/diff.d.ts +0 -21
- package/dist/policy/diff.js +0 -92
- package/dist/policy/diff.js.map +0 -1
- package/dist/policy/format.d.ts +0 -6
- package/dist/policy/format.js +0 -58
- package/dist/policy/format.js.map +0 -1
- package/dist/policy/load.d.ts +0 -32
- package/dist/policy/load.js +0 -62
- package/dist/policy/load.js.map +0 -1
- package/dist/policy/migrate.d.ts +0 -21
- package/dist/policy/migrate.js +0 -68
- package/dist/policy/migrate.js.map +0 -1
- package/dist/policy/schema.d.ts +0 -5
- package/dist/policy/schema.js +0 -19
- package/dist/policy/schema.js.map +0 -1
- package/dist/policy/validate.d.ts +0 -19
- package/dist/policy/validate.js +0 -263
- package/dist/policy/validate.js.map +0 -1
- package/dist/rules/action.d.ts +0 -65
- package/dist/rules/action.js +0 -217
- package/dist/rules/action.js.map +0 -1
- package/dist/rules/audit-query.d.ts +0 -51
- package/dist/rules/audit-query.js +0 -90
- package/dist/rules/audit-query.js.map +0 -1
- package/dist/rules/conflict-analyzer.d.ts +0 -57
- package/dist/rules/conflict-analyzer.js +0 -215
- package/dist/rules/conflict-analyzer.js.map +0 -1
- package/dist/rules/cron-scheduler.d.ts +0 -62
- package/dist/rules/cron-scheduler.js +0 -187
- package/dist/rules/cron-scheduler.js.map +0 -1
- package/dist/rules/destructive.d.ts +0 -20
- package/dist/rules/destructive.js +0 -53
- package/dist/rules/destructive.js.map +0 -1
- package/dist/rules/engine.d.ts +0 -193
- package/dist/rules/engine.js +0 -758
- package/dist/rules/engine.js.map +0 -1
- package/dist/rules/matcher.d.ts +0 -56
- package/dist/rules/matcher.js +0 -231
- package/dist/rules/matcher.js.map +0 -1
- package/dist/rules/pid-file.d.ts +0 -43
- package/dist/rules/pid-file.js +0 -96
- package/dist/rules/pid-file.js.map +0 -1
- package/dist/rules/quiet-hours.d.ts +0 -26
- package/dist/rules/quiet-hours.js +0 -46
- package/dist/rules/quiet-hours.js.map +0 -1
- package/dist/rules/suggest.d.ts +0 -20
- package/dist/rules/suggest.js +0 -96
- package/dist/rules/suggest.js.map +0 -1
- package/dist/rules/throttle.d.ts +0 -61
- package/dist/rules/throttle.js +0 -117
- package/dist/rules/throttle.js.map +0 -1
- package/dist/rules/types.d.ts +0 -117
- package/dist/rules/types.js +0 -35
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/webhook-listener.d.ts +0 -63
- package/dist/rules/webhook-listener.js +0 -224
- package/dist/rules/webhook-listener.js.map +0 -1
- package/dist/rules/webhook-token.d.ts +0 -50
- package/dist/rules/webhook-token.js +0 -91
- package/dist/rules/webhook-token.js.map +0 -1
- package/dist/schema/field-aliases.d.ts +0 -34
- package/dist/schema/field-aliases.js +0 -132
- package/dist/schema/field-aliases.js.map +0 -1
- package/dist/sinks/dispatcher.d.ts +0 -7
- package/dist/sinks/dispatcher.js +0 -13
- package/dist/sinks/dispatcher.js.map +0 -1
- package/dist/sinks/file.d.ts +0 -6
- package/dist/sinks/file.js +0 -20
- package/dist/sinks/file.js.map +0 -1
- package/dist/sinks/format.d.ts +0 -20
- package/dist/sinks/format.js +0 -57
- package/dist/sinks/format.js.map +0 -1
- package/dist/sinks/homeassistant.d.ts +0 -18
- package/dist/sinks/homeassistant.js +0 -45
- package/dist/sinks/homeassistant.js.map +0 -1
- package/dist/sinks/openclaw.d.ts +0 -13
- package/dist/sinks/openclaw.js +0 -34
- package/dist/sinks/openclaw.js.map +0 -1
- package/dist/sinks/stdout.d.ts +0 -4
- package/dist/sinks/stdout.js +0 -6
- package/dist/sinks/stdout.js.map +0 -1
- package/dist/sinks/telegram.d.ts +0 -11
- package/dist/sinks/telegram.js +0 -29
- package/dist/sinks/telegram.js.map +0 -1
- package/dist/sinks/types.d.ts +0 -13
- package/dist/sinks/types.js +0 -2
- package/dist/sinks/types.js.map +0 -1
- package/dist/sinks/webhook.d.ts +0 -6
- package/dist/sinks/webhook.js +0 -23
- package/dist/sinks/webhook.js.map +0 -1
- package/dist/status-sync/manager.d.ts +0 -48
- package/dist/status-sync/manager.js +0 -269
- package/dist/status-sync/manager.js.map +0 -1
- package/dist/utils/arg-parsers.d.ts +0 -16
- package/dist/utils/arg-parsers.js +0 -67
- package/dist/utils/arg-parsers.js.map +0 -1
- package/dist/utils/audit.d.ts +0 -69
- package/dist/utils/audit.js +0 -122
- package/dist/utils/audit.js.map +0 -1
- package/dist/utils/filter.d.ts +0 -81
- package/dist/utils/filter.js +0 -190
- package/dist/utils/filter.js.map +0 -1
- package/dist/utils/flags.d.ts +0 -72
- package/dist/utils/flags.js +0 -187
- package/dist/utils/flags.js.map +0 -1
- package/dist/utils/format.d.ts +0 -9
- package/dist/utils/format.js +0 -118
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/health.d.ts +0 -48
- package/dist/utils/health.js +0 -102
- package/dist/utils/health.js.map +0 -1
- package/dist/utils/help-json.d.ts +0 -39
- package/dist/utils/help-json.js +0 -55
- package/dist/utils/help-json.js.map +0 -1
- package/dist/utils/name-resolver.d.ts +0 -26
- package/dist/utils/name-resolver.js +0 -138
- package/dist/utils/name-resolver.js.map +0 -1
- package/dist/utils/output.d.ts +0 -73
- package/dist/utils/output.js +0 -405
- package/dist/utils/output.js.map +0 -1
- package/dist/utils/quota.d.ts +0 -61
- package/dist/utils/quota.js +0 -228
- package/dist/utils/quota.js.map +0 -1
- package/dist/utils/redact.d.ts +0 -23
- package/dist/utils/redact.js +0 -69
- package/dist/utils/redact.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -72
- package/dist/utils/retry.js +0 -141
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/string.d.ts +0 -2
- package/dist/utils/string.js +0 -23
- package/dist/utils/string.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -5
- package/dist/version.js.map +0 -1
package/dist/commands/doctor.js
DELETED
|
@@ -1,1016 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { execSync } from 'node:child_process';
|
|
5
|
-
import { printJson, isJsonMode, exitWithError } from '../utils/output.js';
|
|
6
|
-
import { getEffectiveCatalog } from '../devices/catalog.js';
|
|
7
|
-
import { configFilePath, listProfiles, readProfileMeta } from '../config.js';
|
|
8
|
-
import { describeCache, resetListCache } from '../devices/cache.js';
|
|
9
|
-
import { DAILY_QUOTA, todayUsage } from '../utils/quota.js';
|
|
10
|
-
import { AGENT_BOOTSTRAP_SCHEMA_VERSION } from './agent-bootstrap.js';
|
|
11
|
-
import { CATALOG_SCHEMA_VERSION } from '../devices/catalog.js';
|
|
12
|
-
import { createSwitchBotMcpServer, listRegisteredTools } from './mcp.js';
|
|
13
|
-
import { resolvePolicyPath, loadPolicyFile, PolicyFileNotFoundError, PolicyYamlParseError, } from '../policy/load.js';
|
|
14
|
-
import { validateLoadedPolicy } from '../policy/validate.js';
|
|
15
|
-
import { selectCredentialStore } from '../credentials/keychain.js';
|
|
16
|
-
import { getActiveProfile } from '../lib/request-context.js';
|
|
17
|
-
import { readDaemonState } from '../lib/daemon-state.js';
|
|
18
|
-
import { isPidAlive } from '../rules/pid-file.js';
|
|
19
|
-
export const DOCTOR_SCHEMA_VERSION = 1;
|
|
20
|
-
async function checkCredentials() {
|
|
21
|
-
const envOk = Boolean(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
|
|
22
|
-
const profile = getActiveProfile() ?? 'default';
|
|
23
|
-
let backendName = 'file';
|
|
24
|
-
let backendLabel = 'file';
|
|
25
|
-
let writable = true;
|
|
26
|
-
let keychainHasProfile = false;
|
|
27
|
-
try {
|
|
28
|
-
const store = await selectCredentialStore();
|
|
29
|
-
const desc = store.describe();
|
|
30
|
-
backendName = store.name;
|
|
31
|
-
backendLabel = desc.backend;
|
|
32
|
-
writable = desc.writable;
|
|
33
|
-
try {
|
|
34
|
-
const creds = await store.get(profile);
|
|
35
|
-
keychainHasProfile = Boolean(creds && creds.token && creds.secret);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
keychainHasProfile = false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// selectCredentialStore falls back to file; a throw here is unexpected but
|
|
43
|
-
// non-fatal — downstream callers degrade to the file path.
|
|
44
|
-
}
|
|
45
|
-
if (envOk) {
|
|
46
|
-
return {
|
|
47
|
-
name: 'credentials',
|
|
48
|
-
status: 'ok',
|
|
49
|
-
detail: {
|
|
50
|
-
source: 'env',
|
|
51
|
-
backend: backendName,
|
|
52
|
-
backendLabel,
|
|
53
|
-
writable,
|
|
54
|
-
profile,
|
|
55
|
-
message: 'env: SWITCHBOT_TOKEN + SWITCHBOT_SECRET',
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
if (keychainHasProfile && backendName !== 'file') {
|
|
60
|
-
return {
|
|
61
|
-
name: 'credentials',
|
|
62
|
-
status: 'ok',
|
|
63
|
-
detail: {
|
|
64
|
-
source: 'keychain',
|
|
65
|
-
backend: backendName,
|
|
66
|
-
backendLabel,
|
|
67
|
-
writable,
|
|
68
|
-
profile,
|
|
69
|
-
message: `keychain (${backendLabel}) has credentials for profile "${profile}"`,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const file = configFilePath();
|
|
74
|
-
if (!fs.existsSync(file)) {
|
|
75
|
-
return {
|
|
76
|
-
name: 'credentials',
|
|
77
|
-
status: 'fail',
|
|
78
|
-
detail: {
|
|
79
|
-
source: 'none',
|
|
80
|
-
backend: backendName,
|
|
81
|
-
backendLabel,
|
|
82
|
-
writable,
|
|
83
|
-
profile,
|
|
84
|
-
message: `No env vars, no keychain entry for profile "${profile}", and no config at ${file}. Run 'switchbot config set-token' or 'switchbot auth keychain set'.`,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
90
|
-
const cfg = JSON.parse(raw);
|
|
91
|
-
if (!cfg.token || !cfg.secret) {
|
|
92
|
-
return {
|
|
93
|
-
name: 'credentials',
|
|
94
|
-
status: 'fail',
|
|
95
|
-
detail: {
|
|
96
|
-
source: 'file',
|
|
97
|
-
backend: backendName,
|
|
98
|
-
backendLabel,
|
|
99
|
-
writable,
|
|
100
|
-
profile,
|
|
101
|
-
message: `Config ${file} missing token/secret.`,
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const status = writable && backendName !== 'file' ? 'warn' : 'ok';
|
|
106
|
-
const hint = status === 'warn'
|
|
107
|
-
? `Consider running 'switchbot auth keychain migrate' to move credentials into ${backendLabel}.`
|
|
108
|
-
: undefined;
|
|
109
|
-
return {
|
|
110
|
-
name: 'credentials',
|
|
111
|
-
status,
|
|
112
|
-
detail: {
|
|
113
|
-
source: 'file',
|
|
114
|
-
backend: backendName,
|
|
115
|
-
backendLabel,
|
|
116
|
-
writable,
|
|
117
|
-
profile,
|
|
118
|
-
message: `file: ${file}`,
|
|
119
|
-
...(hint ? { hint } : {}),
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
return {
|
|
125
|
-
name: 'credentials',
|
|
126
|
-
status: 'fail',
|
|
127
|
-
detail: {
|
|
128
|
-
source: 'file',
|
|
129
|
-
backend: backendName,
|
|
130
|
-
backendLabel,
|
|
131
|
-
writable,
|
|
132
|
-
profile,
|
|
133
|
-
message: `Unreadable config ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function checkProfiles() {
|
|
139
|
-
const dir = path.join(os.homedir(), '.switchbot', 'profiles');
|
|
140
|
-
if (!fs.existsSync(dir)) {
|
|
141
|
-
return { name: 'profiles', status: 'ok', detail: 'no profile dir (default profile only)' };
|
|
142
|
-
}
|
|
143
|
-
const profiles = listProfiles();
|
|
144
|
-
if (profiles.length === 0) {
|
|
145
|
-
return { name: 'profiles', status: 'ok', detail: 'profile dir empty' };
|
|
146
|
-
}
|
|
147
|
-
const labelled = profiles.map((p) => {
|
|
148
|
-
const meta = readProfileMeta(p);
|
|
149
|
-
if (meta?.label)
|
|
150
|
-
return `${p} (${meta.label})`;
|
|
151
|
-
return p;
|
|
152
|
-
});
|
|
153
|
-
return {
|
|
154
|
-
name: 'profiles',
|
|
155
|
-
status: 'ok',
|
|
156
|
-
detail: `found ${profiles.length}: ${labelled.join(', ')}`,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
async function checkClockSkew() {
|
|
160
|
-
// Real probe: HEAD the SwitchBot API endpoint and compare the server's Date
|
|
161
|
-
// header against local time. No auth required for the Date header — the API
|
|
162
|
-
// returns 401 but still stamps the response. Gracefully degrades to
|
|
163
|
-
// probeSource:'none' if offline / no network reachable.
|
|
164
|
-
//
|
|
165
|
-
// Under vitest, only run the probe if fetch has been stubbed (detected via
|
|
166
|
-
// vi.fn marker) — otherwise skip network I/O to keep unrelated tests fast.
|
|
167
|
-
const underVitest = Boolean(process.env.VITEST);
|
|
168
|
-
const fetchFn = globalThis.fetch;
|
|
169
|
-
const fetchIsMocked = Boolean(fetchFn && typeof fetchFn === 'function' && 'mock' in fetchFn);
|
|
170
|
-
if (underVitest && !fetchIsMocked) {
|
|
171
|
-
return {
|
|
172
|
-
name: 'clock',
|
|
173
|
-
status: 'warn',
|
|
174
|
-
detail: { probeSource: 'none', skewMs: null, message: 'skipped: test environment' },
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
const localBefore = Date.now();
|
|
178
|
-
const ctrl = new AbortController();
|
|
179
|
-
const timer = setTimeout(() => ctrl.abort(), 2500);
|
|
180
|
-
try {
|
|
181
|
-
const res = await fetch('https://api.switch-bot.com/v1.1/devices', {
|
|
182
|
-
method: 'HEAD',
|
|
183
|
-
signal: ctrl.signal,
|
|
184
|
-
});
|
|
185
|
-
const localAfter = Date.now();
|
|
186
|
-
const dateHeader = res.headers.get('date');
|
|
187
|
-
if (!dateHeader) {
|
|
188
|
-
return {
|
|
189
|
-
name: 'clock',
|
|
190
|
-
status: 'warn',
|
|
191
|
-
detail: { probeSource: 'api', skewMs: null, message: 'server returned no Date header' },
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
const serverMs = Date.parse(dateHeader);
|
|
195
|
-
if (!Number.isFinite(serverMs)) {
|
|
196
|
-
return {
|
|
197
|
-
name: 'clock',
|
|
198
|
-
status: 'warn',
|
|
199
|
-
detail: { probeSource: 'api', skewMs: null, message: `unparseable Date header: ${dateHeader}` },
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
// Split the round-trip in half to estimate the local instant that matches
|
|
203
|
-
// the server's Date header. HTTP Date resolution is 1s, so treat anything
|
|
204
|
-
// under 2000ms as ok, 2000–60000ms as warn, beyond that as fail (HMAC
|
|
205
|
-
// auth rejects requests with skew > 5 minutes anyway).
|
|
206
|
-
const midpoint = (localBefore + localAfter) / 2;
|
|
207
|
-
const skewMs = Math.round(midpoint - serverMs);
|
|
208
|
-
const absSkew = Math.abs(skewMs);
|
|
209
|
-
const status = absSkew < 2000 ? 'ok' : absSkew < 60_000 ? 'warn' : 'fail';
|
|
210
|
-
return {
|
|
211
|
-
name: 'clock',
|
|
212
|
-
status,
|
|
213
|
-
detail: {
|
|
214
|
-
probeSource: 'api',
|
|
215
|
-
skewMs,
|
|
216
|
-
localIso: new Date(midpoint).toISOString(),
|
|
217
|
-
serverIso: new Date(serverMs).toISOString(),
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
return {
|
|
223
|
-
name: 'clock',
|
|
224
|
-
status: 'warn',
|
|
225
|
-
detail: {
|
|
226
|
-
probeSource: 'none',
|
|
227
|
-
skewMs: null,
|
|
228
|
-
message: `probe failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
clearTimeout(timer);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function checkCatalog() {
|
|
237
|
-
const catalog = getEffectiveCatalog();
|
|
238
|
-
const missingRole = catalog.filter((e) => !e.role).length;
|
|
239
|
-
if (catalog.length === 0) {
|
|
240
|
-
return { name: 'catalog', status: 'fail', detail: 'catalog empty — package corrupt?' };
|
|
241
|
-
}
|
|
242
|
-
const status = missingRole > 0 ? 'warn' : 'ok';
|
|
243
|
-
return {
|
|
244
|
-
name: 'catalog',
|
|
245
|
-
status,
|
|
246
|
-
detail: `${catalog.length} types loaded${missingRole > 0 ? `, ${missingRole} missing role` : ''}`,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
function checkCache() {
|
|
250
|
-
try {
|
|
251
|
-
const info = describeCache();
|
|
252
|
-
const parts = [];
|
|
253
|
-
parts.push(info.list.exists ? `list: ${info.list.path}` : 'list: (none)');
|
|
254
|
-
parts.push(info.status.exists ? `status: ${info.status.entryCount} entries` : 'status: (none)');
|
|
255
|
-
return { name: 'cache', status: 'ok', detail: parts.join(' | ') };
|
|
256
|
-
}
|
|
257
|
-
catch (err) {
|
|
258
|
-
return { name: 'cache', status: 'warn', detail: `cache inspect failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
function checkQuotaFile() {
|
|
262
|
-
const p = path.join(os.homedir(), '.switchbot', 'quota.json');
|
|
263
|
-
if (!fs.existsSync(p)) {
|
|
264
|
-
return {
|
|
265
|
-
name: 'quota',
|
|
266
|
-
status: 'ok',
|
|
267
|
-
detail: {
|
|
268
|
-
path: p,
|
|
269
|
-
percentUsed: 0,
|
|
270
|
-
remaining: DAILY_QUOTA,
|
|
271
|
-
message: 'no quota file yet (will be created on first call)',
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
const raw = fs.readFileSync(p, 'utf-8');
|
|
277
|
-
JSON.parse(raw);
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return {
|
|
281
|
-
name: 'quota',
|
|
282
|
-
status: 'warn',
|
|
283
|
-
detail: { path: p, message: `unreadable/malformed — run 'switchbot quota reset'` },
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
// P9: surface headroom so agents can decide when to slow down or pause.
|
|
287
|
-
// Quota resets at local midnight (the quota counter buckets by local
|
|
288
|
-
// date), so project the next reset to the next 00:00:00 local.
|
|
289
|
-
const usage = todayUsage();
|
|
290
|
-
const percentUsed = Math.round((usage.total / DAILY_QUOTA) * 100);
|
|
291
|
-
const now = new Date();
|
|
292
|
-
const reset = new Date(now);
|
|
293
|
-
reset.setHours(24, 0, 0, 0); // next local midnight
|
|
294
|
-
const status = percentUsed > 80 ? 'warn' : 'ok';
|
|
295
|
-
const recommendation = percentUsed > 90
|
|
296
|
-
? 'over 90% used — consider --no-quota for read-only triage or rescheduling work after the reset'
|
|
297
|
-
: percentUsed > 80
|
|
298
|
-
? 'over 80% used — avoid bulk operations until the daily reset'
|
|
299
|
-
: 'headroom available';
|
|
300
|
-
return {
|
|
301
|
-
name: 'quota',
|
|
302
|
-
status,
|
|
303
|
-
detail: {
|
|
304
|
-
path: p,
|
|
305
|
-
percentUsed,
|
|
306
|
-
remaining: usage.remaining,
|
|
307
|
-
total: usage.total,
|
|
308
|
-
dailyCap: DAILY_QUOTA,
|
|
309
|
-
projectedResetTime: reset.toISOString(),
|
|
310
|
-
recommendation,
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
function checkCatalogSchema() {
|
|
315
|
-
// P9: sentinel against silent drift between the catalog shape and the
|
|
316
|
-
// agent-bootstrap payload. Both constants are exported from their
|
|
317
|
-
// respective modules; if a future refactor changes one without the
|
|
318
|
-
// other, this check fails so consumers (agents) learn before the
|
|
319
|
-
// mismatch corrupts their mental model.
|
|
320
|
-
const match = CATALOG_SCHEMA_VERSION === AGENT_BOOTSTRAP_SCHEMA_VERSION;
|
|
321
|
-
return {
|
|
322
|
-
name: 'catalog-schema',
|
|
323
|
-
status: match ? 'ok' : 'fail',
|
|
324
|
-
detail: {
|
|
325
|
-
catalogSchemaVersion: CATALOG_SCHEMA_VERSION,
|
|
326
|
-
bootstrapExpectsVersion: AGENT_BOOTSTRAP_SCHEMA_VERSION,
|
|
327
|
-
match,
|
|
328
|
-
message: match
|
|
329
|
-
? 'catalog and agent-bootstrap schemaVersion aligned'
|
|
330
|
-
: 'catalog and agent-bootstrap schemaVersion have drifted — bump in lockstep',
|
|
331
|
-
},
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
function checkAudit() {
|
|
335
|
-
// P9: surface recent command failures so agents / ops can spot problems
|
|
336
|
-
// before they page. When --audit-log was never enabled, the file won't
|
|
337
|
-
// exist — report that cleanly rather than as an error.
|
|
338
|
-
const p = path.join(os.homedir(), '.switchbot', 'audit.log');
|
|
339
|
-
if (!fs.existsSync(p)) {
|
|
340
|
-
return {
|
|
341
|
-
name: 'audit',
|
|
342
|
-
status: 'ok',
|
|
343
|
-
detail: {
|
|
344
|
-
path: p,
|
|
345
|
-
enabled: false,
|
|
346
|
-
message: 'audit log not present (enable with --audit-log)',
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
const raw = fs.readFileSync(p, 'utf-8');
|
|
352
|
-
const since = Date.now() - 24 * 60 * 60 * 1000;
|
|
353
|
-
const recent = [];
|
|
354
|
-
let total = 0;
|
|
355
|
-
for (const line of raw.split('\n')) {
|
|
356
|
-
const trimmed = line.trim();
|
|
357
|
-
if (!trimmed)
|
|
358
|
-
continue;
|
|
359
|
-
let rec;
|
|
360
|
-
try {
|
|
361
|
-
rec = JSON.parse(trimmed);
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (rec.result !== 'error')
|
|
367
|
-
continue;
|
|
368
|
-
total += 1;
|
|
369
|
-
const ts = rec.t ? Date.parse(rec.t) : NaN;
|
|
370
|
-
if (Number.isFinite(ts) && ts >= since) {
|
|
371
|
-
recent.push({
|
|
372
|
-
t: rec.t,
|
|
373
|
-
command: rec.command ?? '?',
|
|
374
|
-
deviceId: rec.deviceId,
|
|
375
|
-
error: rec.error ?? 'unknown',
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// Cap the report to the 10 most recent so the doctor payload stays
|
|
380
|
-
// bounded even on a log with thousands of errors.
|
|
381
|
-
recent.sort((a, b) => (a.t < b.t ? 1 : -1));
|
|
382
|
-
const clipped = recent.slice(0, 10);
|
|
383
|
-
const status = recent.length > 0 ? 'warn' : 'ok';
|
|
384
|
-
return {
|
|
385
|
-
name: 'audit',
|
|
386
|
-
status,
|
|
387
|
-
detail: {
|
|
388
|
-
path: p,
|
|
389
|
-
enabled: true,
|
|
390
|
-
totalErrors: total,
|
|
391
|
-
errorsLast24h: recent.length,
|
|
392
|
-
recent: clipped,
|
|
393
|
-
},
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
catch (err) {
|
|
397
|
-
return {
|
|
398
|
-
name: 'audit',
|
|
399
|
-
status: 'warn',
|
|
400
|
-
detail: {
|
|
401
|
-
path: p,
|
|
402
|
-
enabled: true,
|
|
403
|
-
message: `could not read audit log: ${err instanceof Error ? err.message : String(err)}`,
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
function checkPolicy() {
|
|
409
|
-
// A policy file is optional — many users run the CLI without one. Report
|
|
410
|
-
// `ok` with `present: false` so agents can tell the difference between
|
|
411
|
-
// "no policy configured" (fine) and "policy broken" (needs attention).
|
|
412
|
-
const policyPath = resolvePolicyPath();
|
|
413
|
-
try {
|
|
414
|
-
const loaded = loadPolicyFile(policyPath);
|
|
415
|
-
const result = validateLoadedPolicy(loaded);
|
|
416
|
-
if (result.valid) {
|
|
417
|
-
return {
|
|
418
|
-
name: 'policy',
|
|
419
|
-
status: 'ok',
|
|
420
|
-
detail: {
|
|
421
|
-
path: policyPath,
|
|
422
|
-
present: true,
|
|
423
|
-
valid: true,
|
|
424
|
-
schemaVersion: result.schemaVersion,
|
|
425
|
-
},
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
return {
|
|
429
|
-
name: 'policy',
|
|
430
|
-
status: 'fail',
|
|
431
|
-
detail: {
|
|
432
|
-
path: policyPath,
|
|
433
|
-
present: true,
|
|
434
|
-
valid: false,
|
|
435
|
-
schemaVersion: result.schemaVersion,
|
|
436
|
-
errorCount: result.errors.length,
|
|
437
|
-
firstError: result.errors[0]
|
|
438
|
-
? {
|
|
439
|
-
path: result.errors[0].path,
|
|
440
|
-
line: result.errors[0].line,
|
|
441
|
-
message: result.errors[0].message,
|
|
442
|
-
}
|
|
443
|
-
: undefined,
|
|
444
|
-
message: "run 'switchbot policy validate' for full diagnostics",
|
|
445
|
-
},
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
catch (err) {
|
|
449
|
-
if (err instanceof PolicyFileNotFoundError) {
|
|
450
|
-
return {
|
|
451
|
-
name: 'policy',
|
|
452
|
-
status: 'ok',
|
|
453
|
-
detail: {
|
|
454
|
-
path: policyPath,
|
|
455
|
-
present: false,
|
|
456
|
-
message: "no policy file (optional — run 'switchbot policy new' to scaffold one)",
|
|
457
|
-
},
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
if (err instanceof PolicyYamlParseError) {
|
|
461
|
-
const first = err.yamlErrors[0];
|
|
462
|
-
return {
|
|
463
|
-
name: 'policy',
|
|
464
|
-
status: 'fail',
|
|
465
|
-
detail: {
|
|
466
|
-
path: policyPath,
|
|
467
|
-
present: true,
|
|
468
|
-
valid: false,
|
|
469
|
-
parseError: true,
|
|
470
|
-
line: first?.line,
|
|
471
|
-
col: first?.col,
|
|
472
|
-
message: first?.message ?? err.message,
|
|
473
|
-
},
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
return {
|
|
477
|
-
name: 'policy',
|
|
478
|
-
status: 'warn',
|
|
479
|
-
detail: {
|
|
480
|
-
path: policyPath,
|
|
481
|
-
message: `could not read policy file: ${err instanceof Error ? err.message : String(err)}`,
|
|
482
|
-
},
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
async function checkKeychain() {
|
|
487
|
-
try {
|
|
488
|
-
const { selectCredentialStore } = await import('../credentials/keychain.js');
|
|
489
|
-
const store = await selectCredentialStore();
|
|
490
|
-
const desc = store.describe();
|
|
491
|
-
const isNative = desc.backend !== 'file';
|
|
492
|
-
if (!isNative) {
|
|
493
|
-
// Native keychain not available or not detected
|
|
494
|
-
return {
|
|
495
|
-
name: 'keychain',
|
|
496
|
-
status: 'warn',
|
|
497
|
-
detail: {
|
|
498
|
-
backend: desc.backend,
|
|
499
|
-
message: 'OS native keychain not detected — credentials stored in plain file (~/.switchbot/config.json). Consider installing a keychain backend for better security.',
|
|
500
|
-
hint: process.platform === 'linux'
|
|
501
|
-
? 'Install libsecret (secret-tool) for GNOME Keyring support.'
|
|
502
|
-
: process.platform === 'darwin'
|
|
503
|
-
? 'macOS Keychain is available — re-run `switchbot config set-token` to store credentials there.'
|
|
504
|
-
: 'Windows Credential Manager is available — re-run `switchbot config set-token` to use it.',
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
name: 'keychain',
|
|
510
|
-
status: 'ok',
|
|
511
|
-
detail: {
|
|
512
|
-
backend: desc.backend,
|
|
513
|
-
writable: desc.writable,
|
|
514
|
-
message: `Credentials stored in OS keychain (${desc.backend}).`,
|
|
515
|
-
},
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
catch (err) {
|
|
519
|
-
return {
|
|
520
|
-
name: 'keychain',
|
|
521
|
-
status: 'warn',
|
|
522
|
-
detail: { message: `Keychain probe failed: ${err instanceof Error ? err.message : String(err)}` },
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
function checkNodeVersion() {
|
|
527
|
-
const major = Number(process.versions.node.split('.')[0]);
|
|
528
|
-
if (Number.isFinite(major) && major < 18) {
|
|
529
|
-
return { name: 'node', status: 'fail', detail: `Node ${process.versions.node} — minimum is 18` };
|
|
530
|
-
}
|
|
531
|
-
return { name: 'node', status: 'ok', detail: `Node ${process.versions.node}` };
|
|
532
|
-
}
|
|
533
|
-
function detectShellFlavor() {
|
|
534
|
-
const shell = (process.env.SHELL ?? '').toLowerCase();
|
|
535
|
-
const comspec = (process.env.COMSPEC ?? '').toLowerCase();
|
|
536
|
-
if (shell.includes('pwsh') || shell.includes('powershell'))
|
|
537
|
-
return 'powershell';
|
|
538
|
-
if (comspec.includes('powershell') || comspec.includes('pwsh'))
|
|
539
|
-
return 'powershell';
|
|
540
|
-
if (comspec.endsWith('cmd.exe'))
|
|
541
|
-
return 'cmd';
|
|
542
|
-
if (shell.endsWith('/fish') || shell === 'fish')
|
|
543
|
-
return 'fish';
|
|
544
|
-
if (shell.endsWith('/zsh') || shell === 'zsh')
|
|
545
|
-
return 'zsh';
|
|
546
|
-
if (shell.endsWith('/bash') || shell === 'bash')
|
|
547
|
-
return 'bash';
|
|
548
|
-
return process.platform === 'win32' ? 'powershell' : 'unknown';
|
|
549
|
-
}
|
|
550
|
-
function buildPathFix(shell, missingSegment, npmBinDir) {
|
|
551
|
-
if (!npmBinDir) {
|
|
552
|
-
return process.platform === 'win32'
|
|
553
|
-
? 'Run: npm prefix -g and add that directory to your PATH.'
|
|
554
|
-
: 'Run: npm prefix -g and add <prefix>/bin to your PATH.';
|
|
555
|
-
}
|
|
556
|
-
switch (shell) {
|
|
557
|
-
case 'powershell':
|
|
558
|
-
return `$env:Path = "${missingSegment};" + $env:Path # persist via your PowerShell profile or System Properties`;
|
|
559
|
-
case 'cmd':
|
|
560
|
-
return `set PATH=${missingSegment};%PATH% && setx PATH "${missingSegment};%PATH%"`;
|
|
561
|
-
case 'fish':
|
|
562
|
-
return `fish_add_path "${missingSegment}"`;
|
|
563
|
-
case 'zsh':
|
|
564
|
-
return `export PATH="${missingSegment}:$PATH" # add to ~/.zshrc`;
|
|
565
|
-
case 'bash':
|
|
566
|
-
return `export PATH="${missingSegment}:$PATH" # add to ~/.bashrc`;
|
|
567
|
-
default:
|
|
568
|
-
return `export PATH="${missingSegment}:$PATH"`;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
function checkPathDiscoverability() {
|
|
572
|
-
// Detect whether the `switchbot` binary is reachable on PATH.
|
|
573
|
-
// This catches the common "npm install -g worked but PATH not updated" failure.
|
|
574
|
-
const isWindows = process.platform === 'win32';
|
|
575
|
-
const binaryName = isWindows ? 'switchbot.cmd' : 'switchbot';
|
|
576
|
-
// Find where npm puts global bins.
|
|
577
|
-
let npmBinDir = null;
|
|
578
|
-
try {
|
|
579
|
-
const prefix = execSync('npm prefix -g', { timeout: 4000, encoding: 'utf-8' }).trim();
|
|
580
|
-
npmBinDir = isWindows ? prefix : path.join(prefix, 'bin');
|
|
581
|
-
}
|
|
582
|
-
catch {
|
|
583
|
-
// npm not on PATH or other error; fall through.
|
|
584
|
-
}
|
|
585
|
-
// Check whether `switchbot` resolves via PATH.
|
|
586
|
-
let binaryOnPath = false;
|
|
587
|
-
let resolvedPath = null;
|
|
588
|
-
try {
|
|
589
|
-
const which = execSync(isWindows ? `where ${binaryName}` : `which ${binaryName}`, { timeout: 3000, encoding: 'utf-8' }).trim().split(/\r?\n/)[0];
|
|
590
|
-
if (which) {
|
|
591
|
-
binaryOnPath = true;
|
|
592
|
-
resolvedPath = which;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
catch {
|
|
596
|
-
binaryOnPath = false;
|
|
597
|
-
}
|
|
598
|
-
if (binaryOnPath) {
|
|
599
|
-
return {
|
|
600
|
-
name: 'path',
|
|
601
|
-
status: 'ok',
|
|
602
|
-
detail: {
|
|
603
|
-
binaryOnPath: true,
|
|
604
|
-
resolvedPath,
|
|
605
|
-
npmBinDir,
|
|
606
|
-
message: `switchbot is reachable at ${resolvedPath}`,
|
|
607
|
-
},
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
// Not on PATH — figure out what the user should add.
|
|
611
|
-
const currentPath = process.env.PATH ?? '';
|
|
612
|
-
const missingSegment = npmBinDir && !currentPath.split(path.delimiter).includes(npmBinDir)
|
|
613
|
-
? npmBinDir
|
|
614
|
-
: null;
|
|
615
|
-
const currentShell = detectShellFlavor();
|
|
616
|
-
const shellFix = buildPathFix(currentShell, missingSegment, npmBinDir);
|
|
617
|
-
return {
|
|
618
|
-
name: 'path',
|
|
619
|
-
status: 'warn',
|
|
620
|
-
detail: {
|
|
621
|
-
binaryOnPath: false,
|
|
622
|
-
resolvedPath: null,
|
|
623
|
-
npmBinDir,
|
|
624
|
-
missingPathSegment: missingSegment,
|
|
625
|
-
currentShell,
|
|
626
|
-
fix: shellFix,
|
|
627
|
-
message: `'switchbot' is not on PATH. ${shellFix}`,
|
|
628
|
-
},
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
function checkDaemon() {
|
|
632
|
-
const state = readDaemonState();
|
|
633
|
-
if (!state) {
|
|
634
|
-
return {
|
|
635
|
-
name: 'daemon',
|
|
636
|
-
status: 'warn',
|
|
637
|
-
detail: {
|
|
638
|
-
present: false,
|
|
639
|
-
message: 'No daemon state file found. Start one with `switchbot daemon start` if you want long-running automation.',
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
const pid = state.pid;
|
|
644
|
-
const running = pid !== null && (pid === process.pid || isPidAlive(pid));
|
|
645
|
-
return {
|
|
646
|
-
name: 'daemon',
|
|
647
|
-
status: running ? 'ok' : 'warn',
|
|
648
|
-
detail: {
|
|
649
|
-
present: true,
|
|
650
|
-
status: running ? 'running' : state.status,
|
|
651
|
-
pid: running ? pid : null,
|
|
652
|
-
stateFile: state.stateFile,
|
|
653
|
-
pidFile: state.pidFile,
|
|
654
|
-
logFile: state.logFile,
|
|
655
|
-
startedAt: state.startedAt ?? null,
|
|
656
|
-
stoppedAt: state.stoppedAt ?? null,
|
|
657
|
-
lastReloadAt: state.lastReloadAt ?? null,
|
|
658
|
-
lastReloadStatus: state.lastReloadStatus ?? null,
|
|
659
|
-
healthConfigured: typeof state.healthzPort === 'number',
|
|
660
|
-
healthzPort: state.healthzPort ?? null,
|
|
661
|
-
message: running
|
|
662
|
-
? 'daemon running'
|
|
663
|
-
: 'daemon not running; use `switchbot daemon start` for long-running automation',
|
|
664
|
-
},
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
async function checkHealthEndpoint() {
|
|
668
|
-
const state = readDaemonState();
|
|
669
|
-
if (!state || typeof state.healthzPort !== 'number') {
|
|
670
|
-
return {
|
|
671
|
-
name: 'health',
|
|
672
|
-
status: 'warn',
|
|
673
|
-
detail: {
|
|
674
|
-
present: false,
|
|
675
|
-
message: 'No health endpoint configured. Start the daemon with `--healthz-port <n>` to enable it.',
|
|
676
|
-
},
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
const url = `http://127.0.0.1:${state.healthzPort}/healthz`;
|
|
680
|
-
try {
|
|
681
|
-
const res = await fetch(url);
|
|
682
|
-
const body = await res.json();
|
|
683
|
-
const overall = body?.data?.overall ?? 'unknown';
|
|
684
|
-
return {
|
|
685
|
-
name: 'health',
|
|
686
|
-
status: res.ok && overall !== 'down' ? 'ok' : 'warn',
|
|
687
|
-
detail: {
|
|
688
|
-
present: true,
|
|
689
|
-
url,
|
|
690
|
-
httpStatus: res.status,
|
|
691
|
-
overall,
|
|
692
|
-
message: res.ok ? `health endpoint reachable at ${url}` : `health endpoint returned HTTP ${res.status}`,
|
|
693
|
-
},
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
catch (err) {
|
|
697
|
-
return {
|
|
698
|
-
name: 'health',
|
|
699
|
-
status: 'warn',
|
|
700
|
-
detail: {
|
|
701
|
-
present: true,
|
|
702
|
-
url,
|
|
703
|
-
message: `health endpoint probe failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
704
|
-
},
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
function checkMqtt() {
|
|
709
|
-
// MQTT credentials are auto-provisioned from the SwitchBot API using the
|
|
710
|
-
// account's token+secret — no extra env vars needed. Report availability
|
|
711
|
-
// based on whether REST credentials are configured (no network call).
|
|
712
|
-
const hasEnvCreds = Boolean(process.env.SWITCHBOT_TOKEN && process.env.SWITCHBOT_SECRET);
|
|
713
|
-
if (hasEnvCreds) {
|
|
714
|
-
return {
|
|
715
|
-
name: 'mqtt',
|
|
716
|
-
status: 'ok',
|
|
717
|
-
detail: "auto-provisioned from credentials — run 'switchbot events mqtt-tail' to test live connectivity",
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
const file = configFilePath();
|
|
721
|
-
if (fs.existsSync(file)) {
|
|
722
|
-
try {
|
|
723
|
-
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
724
|
-
if (cfg.token && cfg.secret) {
|
|
725
|
-
return {
|
|
726
|
-
name: 'mqtt',
|
|
727
|
-
status: 'ok',
|
|
728
|
-
detail: "auto-provisioned from credentials — run 'switchbot events mqtt-tail' to test live connectivity",
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
catch { /* fall through */ }
|
|
733
|
-
}
|
|
734
|
-
return {
|
|
735
|
-
name: 'mqtt',
|
|
736
|
-
status: 'warn',
|
|
737
|
-
detail: "unavailable — configure credentials first (see credentials check above)",
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
async function checkMqttProbe() {
|
|
741
|
-
// P10: live-probe the MQTT broker. Only runs when --probe is passed.
|
|
742
|
-
// Does not subscribe — just connects + disconnects to verify the
|
|
743
|
-
// credential + TLS handshake works end-to-end. Hard 5s timeout so
|
|
744
|
-
// a misbehaving broker never wedges the doctor command.
|
|
745
|
-
const { fetchMqttCredential } = await import('../mqtt/credential.js');
|
|
746
|
-
const { SwitchBotMqttClient } = await import('../mqtt/client.js');
|
|
747
|
-
const token = process.env.SWITCHBOT_TOKEN;
|
|
748
|
-
const secret = process.env.SWITCHBOT_SECRET;
|
|
749
|
-
let creds = null;
|
|
750
|
-
if (token && secret) {
|
|
751
|
-
creds = { token, secret };
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
const file = configFilePath();
|
|
755
|
-
if (fs.existsSync(file)) {
|
|
756
|
-
try {
|
|
757
|
-
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
758
|
-
if (cfg.token && cfg.secret) {
|
|
759
|
-
creds = { token: cfg.token, secret: cfg.secret };
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
catch { /* fall through */ }
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
if (!creds) {
|
|
766
|
-
return {
|
|
767
|
-
name: 'mqtt',
|
|
768
|
-
status: 'warn',
|
|
769
|
-
detail: { probe: 'skipped', reason: 'no credentials configured' },
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
const deadline = new Promise((_, reject) => setTimeout(() => reject(new Error('probe timeout after 5000ms')), 5000));
|
|
773
|
-
try {
|
|
774
|
-
const cred = await Promise.race([fetchMqttCredential(creds.token, creds.secret), deadline]);
|
|
775
|
-
const client = new SwitchBotMqttClient(cred);
|
|
776
|
-
await Promise.race([client.connect(), deadline]);
|
|
777
|
-
await client.disconnect();
|
|
778
|
-
return {
|
|
779
|
-
name: 'mqtt',
|
|
780
|
-
status: 'ok',
|
|
781
|
-
detail: { probe: 'connected', brokerUrl: cred.brokerUrl, region: cred.region },
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
catch (err) {
|
|
785
|
-
return {
|
|
786
|
-
name: 'mqtt',
|
|
787
|
-
status: 'warn',
|
|
788
|
-
detail: { probe: 'failed', reason: err instanceof Error ? err.message : String(err) },
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function checkMcp() {
|
|
793
|
-
// P10: dry-run instantiation of the MCP server to catch tool-registration
|
|
794
|
-
// regressions. No network I/O, no token needed. If createSwitchBotMcpServer
|
|
795
|
-
// throws (e.g. duplicate tool name, schema build error) the check fails.
|
|
796
|
-
try {
|
|
797
|
-
const server = createSwitchBotMcpServer();
|
|
798
|
-
const tools = listRegisteredTools(server);
|
|
799
|
-
return {
|
|
800
|
-
name: 'mcp',
|
|
801
|
-
status: 'ok',
|
|
802
|
-
detail: {
|
|
803
|
-
serverInstantiated: true,
|
|
804
|
-
toolCount: tools.length,
|
|
805
|
-
tools,
|
|
806
|
-
transportsAvailable: ['stdio', 'http'],
|
|
807
|
-
message: `${tools.length} tools registered; no network probe`,
|
|
808
|
-
},
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
catch (err) {
|
|
812
|
-
return {
|
|
813
|
-
name: 'mcp',
|
|
814
|
-
status: 'fail',
|
|
815
|
-
detail: {
|
|
816
|
-
serverInstantiated: false,
|
|
817
|
-
error: err instanceof Error ? err.message : String(err),
|
|
818
|
-
},
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
const CHECK_REGISTRY = [
|
|
823
|
-
{ name: 'node', description: 'Node.js version compatibility', run: () => checkNodeVersion() },
|
|
824
|
-
{ name: 'path', description: 'switchbot binary reachable on PATH', run: () => checkPathDiscoverability() },
|
|
825
|
-
{ name: 'credentials', description: 'credentials file present and parseable', run: () => checkCredentials() },
|
|
826
|
-
{ name: 'keychain', description: 'OS keychain backend availability and usage', run: () => checkKeychain() },
|
|
827
|
-
{ name: 'profiles', description: 'profile definitions valid', run: () => checkProfiles() },
|
|
828
|
-
{ name: 'catalog', description: 'catalog loads', run: () => checkCatalog() },
|
|
829
|
-
{ name: 'catalog-schema', description: 'catalog vs agent-bootstrap version aligned', run: () => checkCatalogSchema() },
|
|
830
|
-
{ name: 'cache', description: 'device cache state', run: () => checkCache() },
|
|
831
|
-
{ name: 'quota', description: 'API quota headroom', run: () => checkQuotaFile() },
|
|
832
|
-
{ name: 'clock', description: 'system clock skew', run: () => checkClockSkew() },
|
|
833
|
-
{
|
|
834
|
-
name: 'mqtt',
|
|
835
|
-
description: 'MQTT credentials (+ --probe for live broker handshake)',
|
|
836
|
-
run: ({ probe }) => (probe ? checkMqttProbe() : checkMqtt()),
|
|
837
|
-
},
|
|
838
|
-
{ name: 'mcp', description: 'MCP server instantiable + tool count', run: () => checkMcp() },
|
|
839
|
-
{ name: 'policy', description: 'policy.yaml present + schema-valid (if configured)', run: () => checkPolicy() },
|
|
840
|
-
{ name: 'audit', description: 'recent command errors (last 24h)', run: () => checkAudit() },
|
|
841
|
-
{ name: 'daemon', description: 'daemon state file + runtime status', run: () => checkDaemon() },
|
|
842
|
-
{ name: 'health', description: 'health endpoint availability (daemon --healthz-port)', run: () => checkHealthEndpoint() },
|
|
843
|
-
];
|
|
844
|
-
function applyFixes(checks, writeOk) {
|
|
845
|
-
const results = [];
|
|
846
|
-
for (const c of checks) {
|
|
847
|
-
if (c.name === 'cache' && c.status !== 'ok') {
|
|
848
|
-
if (writeOk) {
|
|
849
|
-
try {
|
|
850
|
-
resetListCache();
|
|
851
|
-
results.push({ check: 'cache', action: 'cache-cleared', applied: true });
|
|
852
|
-
}
|
|
853
|
-
catch (err) {
|
|
854
|
-
results.push({
|
|
855
|
-
check: 'cache',
|
|
856
|
-
action: 'cache-clear',
|
|
857
|
-
applied: false,
|
|
858
|
-
message: err instanceof Error ? err.message : String(err),
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
results.push({
|
|
864
|
-
check: 'cache',
|
|
865
|
-
action: 'cache-clear',
|
|
866
|
-
applied: false,
|
|
867
|
-
message: 'pass --yes to apply',
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
else if (c.name === 'catalog-schema' && c.status !== 'ok') {
|
|
872
|
-
results.push({
|
|
873
|
-
check: 'catalog-schema',
|
|
874
|
-
action: 'manual',
|
|
875
|
-
applied: false,
|
|
876
|
-
message: "drift detected — run 'switchbot capabilities --reload' to refresh overlay",
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
else if (c.name === 'credentials' && c.status === 'fail') {
|
|
880
|
-
results.push({
|
|
881
|
-
check: 'credentials',
|
|
882
|
-
action: 'manual',
|
|
883
|
-
applied: false,
|
|
884
|
-
message: "run 'switchbot config set-token' to configure credentials",
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
return results;
|
|
889
|
-
}
|
|
890
|
-
export function registerDoctorCommand(program) {
|
|
891
|
-
program
|
|
892
|
-
.command('doctor')
|
|
893
|
-
.description('Self-check the SwitchBot CLI setup: credentials, catalog, cache, quota, MQTT, daemon, health, MCP')
|
|
894
|
-
.option('--section <names>', 'Comma-separated list of checks to run (see --list for names)')
|
|
895
|
-
.option('--list', 'Print the registered check names and exit 0 without running any check')
|
|
896
|
-
.option('--fix', 'Apply safe, reversible remediations for failing checks (e.g. clear stale cache)')
|
|
897
|
-
.option('--yes', 'Required together with --fix to confirm write actions')
|
|
898
|
-
.option('--probe', 'Perform live-probe variant of checks that support it (mqtt)')
|
|
899
|
-
.addHelpText('after', `
|
|
900
|
-
Runs a battery of local sanity checks and exits with code 0 only when every
|
|
901
|
-
check is 'ok'. 'warn' → exit 0 (informational); 'fail' → exit 1.
|
|
902
|
-
|
|
903
|
-
Examples:
|
|
904
|
-
$ switchbot doctor
|
|
905
|
-
$ switchbot --json doctor | jq '.checks[] | select(.status != "ok")'
|
|
906
|
-
$ switchbot doctor --list
|
|
907
|
-
$ switchbot doctor --section credentials,mcp --json
|
|
908
|
-
$ switchbot doctor --section daemon,health --json
|
|
909
|
-
$ switchbot doctor --probe --json
|
|
910
|
-
$ switchbot doctor --fix --yes --json
|
|
911
|
-
`)
|
|
912
|
-
.action(async (opts) => {
|
|
913
|
-
// --list: print the registry and exit 0.
|
|
914
|
-
if (opts.list) {
|
|
915
|
-
if (isJsonMode()) {
|
|
916
|
-
printJson({
|
|
917
|
-
checks: CHECK_REGISTRY.map((c) => ({ name: c.name, description: c.description })),
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
console.log('Available checks:');
|
|
922
|
-
for (const c of CHECK_REGISTRY) {
|
|
923
|
-
console.log(` ${c.name.padEnd(16)} ${c.description}`);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
// --section: run only the named subset, dedup and validate.
|
|
929
|
-
let selected = CHECK_REGISTRY;
|
|
930
|
-
if (opts.section) {
|
|
931
|
-
const raw = opts.section.split(',').map((s) => s.trim()).filter(Boolean);
|
|
932
|
-
const names = Array.from(new Set(raw));
|
|
933
|
-
const known = new Set(CHECK_REGISTRY.map((c) => c.name));
|
|
934
|
-
const unknown = names.filter((n) => !known.has(n));
|
|
935
|
-
if (unknown.length > 0) {
|
|
936
|
-
exitWithError({
|
|
937
|
-
code: 2,
|
|
938
|
-
kind: 'usage',
|
|
939
|
-
message: `Unknown check name(s): ${unknown.join(', ')}. Valid: ${CHECK_REGISTRY.map((c) => c.name).join(', ')}`,
|
|
940
|
-
});
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
const order = new Map(CHECK_REGISTRY.map((c, i) => [c.name, i]));
|
|
944
|
-
selected = names
|
|
945
|
-
.map((n) => CHECK_REGISTRY.find((c) => c.name === n))
|
|
946
|
-
.sort((a, b) => (order.get(a.name) - order.get(b.name)));
|
|
947
|
-
}
|
|
948
|
-
const runOpts = { probe: Boolean(opts.probe) };
|
|
949
|
-
const checks = [];
|
|
950
|
-
for (const def of selected) {
|
|
951
|
-
checks.push(await def.run(runOpts));
|
|
952
|
-
}
|
|
953
|
-
const summary = {
|
|
954
|
-
ok: checks.filter((c) => c.status === 'ok').length,
|
|
955
|
-
warn: checks.filter((c) => c.status === 'warn').length,
|
|
956
|
-
fail: checks.filter((c) => c.status === 'fail').length,
|
|
957
|
-
};
|
|
958
|
-
const overallFail = summary.fail > 0;
|
|
959
|
-
const overall = overallFail ? 'fail' : summary.warn > 0 ? 'warn' : 'ok';
|
|
960
|
-
const total = summary.ok + summary.warn + summary.fail;
|
|
961
|
-
const rawScore = total > 0 ? Math.round(((summary.ok + summary.warn * 0.5) / total) * 100) : 100;
|
|
962
|
-
const maturityScore = Math.min(100, Math.max(0, rawScore));
|
|
963
|
-
const maturityLabel = maturityScore >= 90 ? 'production-ready'
|
|
964
|
-
: maturityScore >= 70 ? 'mostly-ready'
|
|
965
|
-
: maturityScore >= 40 ? 'needs-work'
|
|
966
|
-
: 'not-ready';
|
|
967
|
-
let fixes;
|
|
968
|
-
if (opts.fix) {
|
|
969
|
-
fixes = applyFixes(checks, Boolean(opts.yes));
|
|
970
|
-
}
|
|
971
|
-
if (isJsonMode()) {
|
|
972
|
-
// Stable contract (locked as doctor.schemaVersion=1):
|
|
973
|
-
// { ok: boolean, overall: 'ok'|'warn'|'fail', generatedAt, schemaVersion,
|
|
974
|
-
// summary: { ok, warn, fail }, checks: [{ name, status, detail }] }
|
|
975
|
-
// `ok` is an alias of (overall === 'ok') — agents prefer the boolean,
|
|
976
|
-
// humans prefer the string; both are provided.
|
|
977
|
-
const payload = {
|
|
978
|
-
ok: overall === 'ok',
|
|
979
|
-
overall,
|
|
980
|
-
maturityScore,
|
|
981
|
-
maturityLabel,
|
|
982
|
-
generatedAt: new Date().toISOString(),
|
|
983
|
-
schemaVersion: DOCTOR_SCHEMA_VERSION,
|
|
984
|
-
summary,
|
|
985
|
-
checks,
|
|
986
|
-
};
|
|
987
|
-
if (fixes !== undefined)
|
|
988
|
-
payload.fixes = fixes;
|
|
989
|
-
printJson(payload);
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
for (const c of checks) {
|
|
993
|
-
const icon = c.status === 'ok' ? '✓' : c.status === 'warn' ? '!' : '✗';
|
|
994
|
-
const detailStr = typeof c.detail === 'string'
|
|
995
|
-
? c.detail
|
|
996
|
-
: (typeof c.detail.message === 'string'
|
|
997
|
-
? (c.detail.message)
|
|
998
|
-
: JSON.stringify(c.detail));
|
|
999
|
-
console.log(`${icon} ${c.name.padEnd(12)} ${detailStr}`);
|
|
1000
|
-
}
|
|
1001
|
-
console.log('');
|
|
1002
|
-
console.log(`${summary.ok} ok, ${summary.warn} warn, ${summary.fail} fail`);
|
|
1003
|
-
if (fixes && fixes.length > 0) {
|
|
1004
|
-
console.log('');
|
|
1005
|
-
console.log('Fixes:');
|
|
1006
|
-
for (const f of fixes) {
|
|
1007
|
-
const marker = f.applied ? '✓' : '-';
|
|
1008
|
-
console.log(` ${marker} ${f.check}: ${f.action}${f.message ? ' — ' + f.message : ''}`);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
if (overallFail)
|
|
1013
|
-
process.exit(1);
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
//# sourceMappingURL=doctor.js.map
|