@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.
Files changed (332) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +57349 -170
  3. package/package.json +9 -5
  4. package/dist/api/client.d.ts +0 -31
  5. package/dist/api/client.js +0 -236
  6. package/dist/api/client.js.map +0 -1
  7. package/dist/auth.d.ts +0 -1
  8. package/dist/auth.js +0 -21
  9. package/dist/auth.js.map +0 -1
  10. package/dist/commands/agent-bootstrap.d.ts +0 -10
  11. package/dist/commands/agent-bootstrap.js +0 -200
  12. package/dist/commands/agent-bootstrap.js.map +0 -1
  13. package/dist/commands/auth.d.ts +0 -18
  14. package/dist/commands/auth.js +0 -355
  15. package/dist/commands/auth.js.map +0 -1
  16. package/dist/commands/batch.d.ts +0 -2
  17. package/dist/commands/batch.js +0 -414
  18. package/dist/commands/batch.js.map +0 -1
  19. package/dist/commands/cache.d.ts +0 -2
  20. package/dist/commands/cache.js +0 -127
  21. package/dist/commands/cache.js.map +0 -1
  22. package/dist/commands/capabilities.d.ts +0 -31
  23. package/dist/commands/capabilities.js +0 -383
  24. package/dist/commands/capabilities.js.map +0 -1
  25. package/dist/commands/catalog.d.ts +0 -2
  26. package/dist/commands/catalog.js +0 -360
  27. package/dist/commands/catalog.js.map +0 -1
  28. package/dist/commands/completion.d.ts +0 -2
  29. package/dist/commands/completion.js +0 -386
  30. package/dist/commands/completion.js.map +0 -1
  31. package/dist/commands/config.d.ts +0 -21
  32. package/dist/commands/config.js +0 -377
  33. package/dist/commands/config.js.map +0 -1
  34. package/dist/commands/daemon.d.ts +0 -2
  35. package/dist/commands/daemon.js +0 -411
  36. package/dist/commands/daemon.js.map +0 -1
  37. package/dist/commands/device-meta.d.ts +0 -2
  38. package/dist/commands/device-meta.js +0 -160
  39. package/dist/commands/device-meta.js.map +0 -1
  40. package/dist/commands/devices.d.ts +0 -2
  41. package/dist/commands/devices.js +0 -949
  42. package/dist/commands/devices.js.map +0 -1
  43. package/dist/commands/doctor.d.ts +0 -3
  44. package/dist/commands/doctor.js +0 -1016
  45. package/dist/commands/doctor.js.map +0 -1
  46. package/dist/commands/events.d.ts +0 -31
  47. package/dist/commands/events.js +0 -564
  48. package/dist/commands/events.js.map +0 -1
  49. package/dist/commands/expand.d.ts +0 -2
  50. package/dist/commands/expand.js +0 -131
  51. package/dist/commands/expand.js.map +0 -1
  52. package/dist/commands/explain.d.ts +0 -2
  53. package/dist/commands/explain.js +0 -140
  54. package/dist/commands/explain.js.map +0 -1
  55. package/dist/commands/health.d.ts +0 -8
  56. package/dist/commands/health.js +0 -114
  57. package/dist/commands/health.js.map +0 -1
  58. package/dist/commands/history.d.ts +0 -2
  59. package/dist/commands/history.js +0 -321
  60. package/dist/commands/history.js.map +0 -1
  61. package/dist/commands/identity.d.ts +0 -45
  62. package/dist/commands/identity.js +0 -60
  63. package/dist/commands/identity.js.map +0 -1
  64. package/dist/commands/install.d.ts +0 -20
  65. package/dist/commands/install.js +0 -247
  66. package/dist/commands/install.js.map +0 -1
  67. package/dist/commands/mcp.d.ts +0 -14
  68. package/dist/commands/mcp.js +0 -2018
  69. package/dist/commands/mcp.js.map +0 -1
  70. package/dist/commands/plan.d.ts +0 -51
  71. package/dist/commands/plan.js +0 -654
  72. package/dist/commands/plan.js.map +0 -1
  73. package/dist/commands/policy.d.ts +0 -24
  74. package/dist/commands/policy.js +0 -587
  75. package/dist/commands/policy.js.map +0 -1
  76. package/dist/commands/quota.d.ts +0 -2
  77. package/dist/commands/quota.js +0 -79
  78. package/dist/commands/quota.js.map +0 -1
  79. package/dist/commands/rules.d.ts +0 -2
  80. package/dist/commands/rules.js +0 -876
  81. package/dist/commands/rules.js.map +0 -1
  82. package/dist/commands/scenes.d.ts +0 -2
  83. package/dist/commands/scenes.js +0 -265
  84. package/dist/commands/scenes.js.map +0 -1
  85. package/dist/commands/schema.d.ts +0 -2
  86. package/dist/commands/schema.js +0 -185
  87. package/dist/commands/schema.js.map +0 -1
  88. package/dist/commands/status-sync.d.ts +0 -2
  89. package/dist/commands/status-sync.js +0 -132
  90. package/dist/commands/status-sync.js.map +0 -1
  91. package/dist/commands/uninstall.d.ts +0 -20
  92. package/dist/commands/uninstall.js +0 -238
  93. package/dist/commands/uninstall.js.map +0 -1
  94. package/dist/commands/upgrade-check.d.ts +0 -2
  95. package/dist/commands/upgrade-check.js +0 -107
  96. package/dist/commands/upgrade-check.js.map +0 -1
  97. package/dist/commands/watch.d.ts +0 -2
  98. package/dist/commands/watch.js +0 -195
  99. package/dist/commands/watch.js.map +0 -1
  100. package/dist/commands/webhook.d.ts +0 -2
  101. package/dist/commands/webhook.js +0 -183
  102. package/dist/commands/webhook.js.map +0 -1
  103. package/dist/config.d.ts +0 -57
  104. package/dist/config.js +0 -259
  105. package/dist/config.js.map +0 -1
  106. package/dist/credentials/backends/file.d.ts +0 -18
  107. package/dist/credentials/backends/file.js +0 -102
  108. package/dist/credentials/backends/file.js.map +0 -1
  109. package/dist/credentials/backends/linux.d.ts +0 -16
  110. package/dist/credentials/backends/linux.js +0 -130
  111. package/dist/credentials/backends/linux.js.map +0 -1
  112. package/dist/credentials/backends/macos.d.ts +0 -18
  113. package/dist/credentials/backends/macos.js +0 -130
  114. package/dist/credentials/backends/macos.js.map +0 -1
  115. package/dist/credentials/backends/windows.d.ts +0 -23
  116. package/dist/credentials/backends/windows.js +0 -216
  117. package/dist/credentials/backends/windows.js.map +0 -1
  118. package/dist/credentials/keychain.d.ts +0 -83
  119. package/dist/credentials/keychain.js +0 -89
  120. package/dist/credentials/keychain.js.map +0 -1
  121. package/dist/credentials/prime.d.ts +0 -32
  122. package/dist/credentials/prime.js +0 -53
  123. package/dist/credentials/prime.js.map +0 -1
  124. package/dist/devices/cache.d.ts +0 -79
  125. package/dist/devices/cache.js +0 -294
  126. package/dist/devices/cache.js.map +0 -1
  127. package/dist/devices/catalog.d.ts +0 -138
  128. package/dist/devices/catalog.js +0 -768
  129. package/dist/devices/catalog.js.map +0 -1
  130. package/dist/devices/device-meta.d.ts +0 -15
  131. package/dist/devices/device-meta.js +0 -57
  132. package/dist/devices/device-meta.js.map +0 -1
  133. package/dist/devices/history-agg.d.ts +0 -37
  134. package/dist/devices/history-agg.js +0 -139
  135. package/dist/devices/history-agg.js.map +0 -1
  136. package/dist/devices/history-query.d.ts +0 -45
  137. package/dist/devices/history-query.js +0 -182
  138. package/dist/devices/history-query.js.map +0 -1
  139. package/dist/devices/param-validator.d.ts +0 -40
  140. package/dist/devices/param-validator.js +0 -434
  141. package/dist/devices/param-validator.js.map +0 -1
  142. package/dist/devices/resources.d.ts +0 -74
  143. package/dist/devices/resources.js +0 -271
  144. package/dist/devices/resources.js.map +0 -1
  145. package/dist/index.d.ts +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/install/default-steps.d.ts +0 -66
  148. package/dist/install/default-steps.js +0 -258
  149. package/dist/install/default-steps.js.map +0 -1
  150. package/dist/install/preflight.d.ts +0 -60
  151. package/dist/install/preflight.js +0 -213
  152. package/dist/install/preflight.js.map +0 -1
  153. package/dist/install/steps.d.ts +0 -61
  154. package/dist/install/steps.js +0 -68
  155. package/dist/install/steps.js.map +0 -1
  156. package/dist/lib/command-keywords.d.ts +0 -5
  157. package/dist/lib/command-keywords.js +0 -18
  158. package/dist/lib/command-keywords.js.map +0 -1
  159. package/dist/lib/daemon-state.d.ts +0 -24
  160. package/dist/lib/daemon-state.js +0 -47
  161. package/dist/lib/daemon-state.js.map +0 -1
  162. package/dist/lib/destructive-mode.d.ts +0 -2
  163. package/dist/lib/destructive-mode.js +0 -13
  164. package/dist/lib/destructive-mode.js.map +0 -1
  165. package/dist/lib/devices.d.ts +0 -151
  166. package/dist/lib/devices.js +0 -383
  167. package/dist/lib/devices.js.map +0 -1
  168. package/dist/lib/idempotency.d.ts +0 -46
  169. package/dist/lib/idempotency.js +0 -107
  170. package/dist/lib/idempotency.js.map +0 -1
  171. package/dist/lib/plan-store.d.ts +0 -19
  172. package/dist/lib/plan-store.js +0 -69
  173. package/dist/lib/plan-store.js.map +0 -1
  174. package/dist/lib/request-context.d.ts +0 -7
  175. package/dist/lib/request-context.js +0 -13
  176. package/dist/lib/request-context.js.map +0 -1
  177. package/dist/lib/scenes.d.ts +0 -7
  178. package/dist/lib/scenes.js +0 -11
  179. package/dist/lib/scenes.js.map +0 -1
  180. package/dist/logger.d.ts +0 -4
  181. package/dist/logger.js +0 -17
  182. package/dist/logger.js.map +0 -1
  183. package/dist/mcp/device-history.d.ts +0 -36
  184. package/dist/mcp/device-history.js +0 -146
  185. package/dist/mcp/device-history.js.map +0 -1
  186. package/dist/mcp/events-subscription.d.ts +0 -45
  187. package/dist/mcp/events-subscription.js +0 -214
  188. package/dist/mcp/events-subscription.js.map +0 -1
  189. package/dist/mqtt/client.d.ts +0 -25
  190. package/dist/mqtt/client.js +0 -181
  191. package/dist/mqtt/client.js.map +0 -1
  192. package/dist/mqtt/credential.d.ts +0 -16
  193. package/dist/mqtt/credential.js +0 -31
  194. package/dist/mqtt/credential.js.map +0 -1
  195. package/dist/policy/add-rule.d.ts +0 -21
  196. package/dist/policy/add-rule.js +0 -125
  197. package/dist/policy/add-rule.js.map +0 -1
  198. package/dist/policy/diff.d.ts +0 -21
  199. package/dist/policy/diff.js +0 -92
  200. package/dist/policy/diff.js.map +0 -1
  201. package/dist/policy/format.d.ts +0 -6
  202. package/dist/policy/format.js +0 -58
  203. package/dist/policy/format.js.map +0 -1
  204. package/dist/policy/load.d.ts +0 -32
  205. package/dist/policy/load.js +0 -62
  206. package/dist/policy/load.js.map +0 -1
  207. package/dist/policy/migrate.d.ts +0 -21
  208. package/dist/policy/migrate.js +0 -68
  209. package/dist/policy/migrate.js.map +0 -1
  210. package/dist/policy/schema.d.ts +0 -5
  211. package/dist/policy/schema.js +0 -19
  212. package/dist/policy/schema.js.map +0 -1
  213. package/dist/policy/validate.d.ts +0 -19
  214. package/dist/policy/validate.js +0 -263
  215. package/dist/policy/validate.js.map +0 -1
  216. package/dist/rules/action.d.ts +0 -65
  217. package/dist/rules/action.js +0 -217
  218. package/dist/rules/action.js.map +0 -1
  219. package/dist/rules/audit-query.d.ts +0 -51
  220. package/dist/rules/audit-query.js +0 -90
  221. package/dist/rules/audit-query.js.map +0 -1
  222. package/dist/rules/conflict-analyzer.d.ts +0 -57
  223. package/dist/rules/conflict-analyzer.js +0 -215
  224. package/dist/rules/conflict-analyzer.js.map +0 -1
  225. package/dist/rules/cron-scheduler.d.ts +0 -62
  226. package/dist/rules/cron-scheduler.js +0 -187
  227. package/dist/rules/cron-scheduler.js.map +0 -1
  228. package/dist/rules/destructive.d.ts +0 -20
  229. package/dist/rules/destructive.js +0 -53
  230. package/dist/rules/destructive.js.map +0 -1
  231. package/dist/rules/engine.d.ts +0 -193
  232. package/dist/rules/engine.js +0 -758
  233. package/dist/rules/engine.js.map +0 -1
  234. package/dist/rules/matcher.d.ts +0 -56
  235. package/dist/rules/matcher.js +0 -231
  236. package/dist/rules/matcher.js.map +0 -1
  237. package/dist/rules/pid-file.d.ts +0 -43
  238. package/dist/rules/pid-file.js +0 -96
  239. package/dist/rules/pid-file.js.map +0 -1
  240. package/dist/rules/quiet-hours.d.ts +0 -26
  241. package/dist/rules/quiet-hours.js +0 -46
  242. package/dist/rules/quiet-hours.js.map +0 -1
  243. package/dist/rules/suggest.d.ts +0 -20
  244. package/dist/rules/suggest.js +0 -96
  245. package/dist/rules/suggest.js.map +0 -1
  246. package/dist/rules/throttle.d.ts +0 -61
  247. package/dist/rules/throttle.js +0 -117
  248. package/dist/rules/throttle.js.map +0 -1
  249. package/dist/rules/types.d.ts +0 -117
  250. package/dist/rules/types.js +0 -35
  251. package/dist/rules/types.js.map +0 -1
  252. package/dist/rules/webhook-listener.d.ts +0 -63
  253. package/dist/rules/webhook-listener.js +0 -224
  254. package/dist/rules/webhook-listener.js.map +0 -1
  255. package/dist/rules/webhook-token.d.ts +0 -50
  256. package/dist/rules/webhook-token.js +0 -91
  257. package/dist/rules/webhook-token.js.map +0 -1
  258. package/dist/schema/field-aliases.d.ts +0 -34
  259. package/dist/schema/field-aliases.js +0 -132
  260. package/dist/schema/field-aliases.js.map +0 -1
  261. package/dist/sinks/dispatcher.d.ts +0 -7
  262. package/dist/sinks/dispatcher.js +0 -13
  263. package/dist/sinks/dispatcher.js.map +0 -1
  264. package/dist/sinks/file.d.ts +0 -6
  265. package/dist/sinks/file.js +0 -20
  266. package/dist/sinks/file.js.map +0 -1
  267. package/dist/sinks/format.d.ts +0 -20
  268. package/dist/sinks/format.js +0 -57
  269. package/dist/sinks/format.js.map +0 -1
  270. package/dist/sinks/homeassistant.d.ts +0 -18
  271. package/dist/sinks/homeassistant.js +0 -45
  272. package/dist/sinks/homeassistant.js.map +0 -1
  273. package/dist/sinks/openclaw.d.ts +0 -13
  274. package/dist/sinks/openclaw.js +0 -34
  275. package/dist/sinks/openclaw.js.map +0 -1
  276. package/dist/sinks/stdout.d.ts +0 -4
  277. package/dist/sinks/stdout.js +0 -6
  278. package/dist/sinks/stdout.js.map +0 -1
  279. package/dist/sinks/telegram.d.ts +0 -11
  280. package/dist/sinks/telegram.js +0 -29
  281. package/dist/sinks/telegram.js.map +0 -1
  282. package/dist/sinks/types.d.ts +0 -13
  283. package/dist/sinks/types.js +0 -2
  284. package/dist/sinks/types.js.map +0 -1
  285. package/dist/sinks/webhook.d.ts +0 -6
  286. package/dist/sinks/webhook.js +0 -23
  287. package/dist/sinks/webhook.js.map +0 -1
  288. package/dist/status-sync/manager.d.ts +0 -48
  289. package/dist/status-sync/manager.js +0 -269
  290. package/dist/status-sync/manager.js.map +0 -1
  291. package/dist/utils/arg-parsers.d.ts +0 -16
  292. package/dist/utils/arg-parsers.js +0 -67
  293. package/dist/utils/arg-parsers.js.map +0 -1
  294. package/dist/utils/audit.d.ts +0 -69
  295. package/dist/utils/audit.js +0 -122
  296. package/dist/utils/audit.js.map +0 -1
  297. package/dist/utils/filter.d.ts +0 -81
  298. package/dist/utils/filter.js +0 -190
  299. package/dist/utils/filter.js.map +0 -1
  300. package/dist/utils/flags.d.ts +0 -72
  301. package/dist/utils/flags.js +0 -187
  302. package/dist/utils/flags.js.map +0 -1
  303. package/dist/utils/format.d.ts +0 -9
  304. package/dist/utils/format.js +0 -118
  305. package/dist/utils/format.js.map +0 -1
  306. package/dist/utils/health.d.ts +0 -48
  307. package/dist/utils/health.js +0 -102
  308. package/dist/utils/health.js.map +0 -1
  309. package/dist/utils/help-json.d.ts +0 -39
  310. package/dist/utils/help-json.js +0 -55
  311. package/dist/utils/help-json.js.map +0 -1
  312. package/dist/utils/name-resolver.d.ts +0 -26
  313. package/dist/utils/name-resolver.js +0 -138
  314. package/dist/utils/name-resolver.js.map +0 -1
  315. package/dist/utils/output.d.ts +0 -73
  316. package/dist/utils/output.js +0 -405
  317. package/dist/utils/output.js.map +0 -1
  318. package/dist/utils/quota.d.ts +0 -61
  319. package/dist/utils/quota.js +0 -228
  320. package/dist/utils/quota.js.map +0 -1
  321. package/dist/utils/redact.d.ts +0 -23
  322. package/dist/utils/redact.js +0 -69
  323. package/dist/utils/redact.js.map +0 -1
  324. package/dist/utils/retry.d.ts +0 -72
  325. package/dist/utils/retry.js +0 -141
  326. package/dist/utils/retry.js.map +0 -1
  327. package/dist/utils/string.d.ts +0 -2
  328. package/dist/utils/string.js +0 -23
  329. package/dist/utils/string.js.map +0 -1
  330. package/dist/version.d.ts +0 -2
  331. package/dist/version.js +0 -5
  332. package/dist/version.js.map +0 -1
@@ -1,876 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { isJsonMode, printJson, exitWithError, printTable } from '../utils/output.js';
5
- import { loadPolicyFile, resolvePolicyPath, DEFAULT_POLICY_PATH, PolicyFileNotFoundError, PolicyYamlParseError, } from '../policy/load.js';
6
- import { validateLoadedPolicy } from '../policy/validate.js';
7
- import { isWebhookTrigger } from '../rules/types.js';
8
- import { lintRules, RulesEngine } from '../rules/engine.js';
9
- import { analyzeConflicts, } from '../rules/conflict-analyzer.js';
10
- import { tryLoadConfig } from '../config.js';
11
- import { fetchMqttCredential } from '../mqtt/credential.js';
12
- import { SwitchBotMqttClient } from '../mqtt/client.js';
13
- import { WebhookTokenStore } from '../rules/webhook-token.js';
14
- import { suggestRule } from '../rules/suggest.js';
15
- import { getCachedDevice } from '../devices/cache.js';
16
- import { getDefaultPidFilePaths, writePidFile, clearPidFile, consumeReloadSentinel, writeReloadSentinel, readPidFile, sighupSupported, isPidAlive, } from '../rules/pid-file.js';
17
- import { readAudit } from '../utils/audit.js';
18
- import { aggregateRuleAudits, filterRuleAudits, RULE_AUDIT_KINDS, } from '../rules/audit-query.js';
19
- import { parseDurationToMs } from '../devices/history-query.js';
20
- const DEFAULT_AUDIT_PATH = path.join(os.homedir(), '.switchbot', 'audit.log');
21
- function loadAutomation(policyPathFlag) {
22
- const path = resolvePolicyPath({ flag: policyPathFlag });
23
- let loaded;
24
- try {
25
- loaded = loadPolicyFile(path);
26
- }
27
- catch (err) {
28
- if (err instanceof PolicyFileNotFoundError) {
29
- exitWithError({
30
- code: 2,
31
- kind: 'usage',
32
- message: `policy file not found: ${path}`,
33
- extra: { subKind: 'file-not-found' },
34
- });
35
- }
36
- if (err instanceof PolicyYamlParseError) {
37
- exitWithError({
38
- code: 3,
39
- kind: 'runtime',
40
- message: `YAML parse error in ${path}: ${err.message}`,
41
- extra: { subKind: 'yaml-parse', errors: err.yamlErrors },
42
- });
43
- }
44
- throw err;
45
- }
46
- const result = validateLoadedPolicy(loaded);
47
- if (!result.valid) {
48
- exitWithError({
49
- code: 4,
50
- kind: 'runtime',
51
- message: 'policy file failed schema validation. Run `switchbot policy validate` for details.',
52
- extra: { subKind: 'invalid-policy', path },
53
- });
54
- }
55
- const data = (loaded.data ?? {});
56
- const automation = (data.automation ?? null);
57
- const aliases = {};
58
- const rawAliases = data.aliases;
59
- if (rawAliases && typeof rawAliases === 'object') {
60
- for (const [k, v] of Object.entries(rawAliases)) {
61
- if (typeof v === 'string')
62
- aliases[k] = v;
63
- }
64
- }
65
- const rawQH = data.quiet_hours;
66
- const quietHours = rawQH && typeof rawQH.start === 'string' && typeof rawQH.end === 'string'
67
- ? { start: rawQH.start, end: rawQH.end }
68
- : null;
69
- return { path, automation, aliases, schemaVersion: result.schemaVersion, quietHours };
70
- }
71
- function describeTrigger(rule) {
72
- const t = rule.when;
73
- if (t.source === 'mqtt')
74
- return t.device ? `mqtt:${t.event}@${t.device}` : `mqtt:${t.event}`;
75
- if (t.source === 'cron') {
76
- const base = `cron:${t.schedule}`;
77
- return t.days && t.days.length > 0 ? `${base} [${t.days.join(',')}]` : base;
78
- }
79
- return `webhook:${t.path}`;
80
- }
81
- function formatLintHuman(result, schemaVersion) {
82
- const lines = [];
83
- lines.push(`policy schema: v${schemaVersion ?? '?'}`);
84
- lines.push(`rules: ${result.rules.length} valid: ${result.valid} unsupported: ${result.unsupportedCount}`);
85
- for (const r of result.rules) {
86
- lines.push(` [${r.status}] ${r.name}`);
87
- for (const i of r.issues) {
88
- lines.push(` ${i.severity}/${i.code}: ${i.message}`);
89
- }
90
- }
91
- return lines.join('\n');
92
- }
93
- function registerLint(rules) {
94
- rules
95
- .command('lint [path]')
96
- .description('Static-check automation.rules — no MQTT, no API calls.')
97
- .action((pathArg) => {
98
- const loaded = loadAutomation(pathArg);
99
- if (!loaded)
100
- return;
101
- const result = lintRules(loaded.automation);
102
- if (isJsonMode()) {
103
- printJson({
104
- policyPath: loaded.path,
105
- policySchemaVersion: loaded.schemaVersion,
106
- automationEnabled: loaded.automation?.enabled === true,
107
- ...result,
108
- });
109
- }
110
- else {
111
- console.log(formatLintHuman(result, loaded.schemaVersion));
112
- }
113
- process.exit(result.valid ? 0 : 1);
114
- });
115
- }
116
- function registerList(rules) {
117
- rules
118
- .command('list [path]')
119
- .description('List the rules declared in a policy file, with trigger / throttle / dry_run summary.')
120
- .action((pathArg) => {
121
- const loaded = loadAutomation(pathArg);
122
- if (!loaded)
123
- return;
124
- const ruleEntries = (loaded.automation?.rules ?? []).map((r) => ({
125
- name: r.name,
126
- enabled: r.enabled !== false,
127
- trigger: describeTrigger(r),
128
- conditions: r.conditions?.length ?? 0,
129
- actions: r.then.length,
130
- throttle: r.throttle?.max_per ?? null,
131
- dry_run: r.dry_run === true,
132
- }));
133
- if (isJsonMode()) {
134
- printJson({
135
- policyPath: loaded.path,
136
- automationEnabled: loaded.automation?.enabled === true,
137
- rules: ruleEntries,
138
- });
139
- }
140
- else if (ruleEntries.length === 0) {
141
- console.log('No rules in this policy file.');
142
- }
143
- else {
144
- console.log(`automation.enabled: ${loaded.automation?.enabled === true}`);
145
- console.log('name | enabled | trigger | conds | actions | throttle | dry');
146
- for (const r of ruleEntries) {
147
- console.log(`${r.name} | ${r.enabled} | ${r.trigger} | ${r.conditions} | ${r.actions} | ${r.throttle ?? '-'} | ${r.dry_run}`);
148
- }
149
- }
150
- });
151
- }
152
- function registerRun(rules) {
153
- rules
154
- .command('run [path]')
155
- .description('Start the rules engine: subscribe to MQTT and execute matching rules (long-running).')
156
- .option('--dry-run', 'Force every action into dry-run mode, overriding rule-level dry_run=false.')
157
- .option('--token <token>', 'SwitchBot API token (falls back to env / config).')
158
- .option('--secret <secret>', 'SwitchBot API secret (falls back to env / config).')
159
- .option('--max-firings <n>', 'Stop after this many successful fires (test / demo use).', (v) => Number.parseInt(v, 10))
160
- .option('--webhook-port <n>', 'Webhook listener port (default 18790). Pass 0 for an auto-allocated port.', (v) => Number.parseInt(v, 10))
161
- .option('--webhook-host <host>', 'Webhook listener bind address (default 127.0.0.1; set 0.0.0.0 to expose beyond loopback).')
162
- .action(async (pathArg, opts) => {
163
- const loaded = loadAutomation(pathArg);
164
- if (!loaded)
165
- return;
166
- if (loaded.automation?.enabled !== true) {
167
- const msg = 'automation.enabled is not true — nothing to run.';
168
- if (isJsonMode()) {
169
- printJson({ kind: 'control', controlKind: 'disabled', message: msg });
170
- }
171
- else {
172
- console.error(msg);
173
- }
174
- process.exit(0);
175
- }
176
- const lint = lintRules(loaded.automation);
177
- if (!lint.valid) {
178
- if (!isJsonMode()) {
179
- console.error('rules lint failed:');
180
- console.error(formatLintHuman(lint, loaded.schemaVersion));
181
- }
182
- exitWithError({
183
- code: 1,
184
- kind: 'runtime',
185
- message: 'rules lint failed — fix errors before running',
186
- extra: { subKind: 'lint-failed', ...lint },
187
- });
188
- }
189
- // Resolve credentials: CLI flags > env (via tryLoadConfig) > config file.
190
- let token = opts.token;
191
- let secret = opts.secret;
192
- if (!token || !secret) {
193
- const cfg = tryLoadConfig();
194
- if (cfg) {
195
- token = token ?? cfg.token;
196
- secret = secret ?? cfg.secret;
197
- }
198
- }
199
- if (!token || !secret) {
200
- exitWithError({
201
- code: 2,
202
- kind: 'usage',
203
- message: 'SwitchBot token + secret are required. Set SWITCHBOT_TOKEN / SWITCHBOT_SECRET or use `switchbot config set-token`.',
204
- extra: { subKind: 'missing-credentials' },
205
- });
206
- }
207
- const needsWebhook = (loaded.automation?.rules ?? []).some((r) => isWebhookTrigger(r.when) && r.enabled !== false);
208
- const webhookTokenStore = new WebhookTokenStore();
209
- const webhookToken = needsWebhook ? webhookTokenStore.getOrCreate() : undefined;
210
- if (!isJsonMode())
211
- console.error('Fetching MQTT credentials…');
212
- const credential = await fetchMqttCredential(token, secret);
213
- const client = new SwitchBotMqttClient(credential, () => fetchMqttCredential(token, secret));
214
- const engine = new RulesEngine({
215
- automation: loaded.automation,
216
- aliases: loaded.aliases,
217
- mqttClient: client,
218
- mqttCredential: credential,
219
- globalDryRun: opts.dryRun === true,
220
- maxFirings: opts.maxFirings,
221
- webhookToken,
222
- webhookPort: opts.webhookPort,
223
- webhookHost: opts.webhookHost,
224
- });
225
- let stopping = false;
226
- const pidPaths = getDefaultPidFilePaths();
227
- writePidFile(pidPaths.pidFile);
228
- const cleanup = () => {
229
- clearPidFile(pidPaths.pidFile);
230
- // Drop any stale reload sentinel too — this process won't see it.
231
- consumeReloadSentinel(pidPaths.reloadFile);
232
- };
233
- const stop = async (code) => {
234
- if (stopping)
235
- return;
236
- stopping = true;
237
- try {
238
- await engine.stop();
239
- await client.disconnect();
240
- }
241
- finally {
242
- cleanup();
243
- process.exit(code);
244
- }
245
- };
246
- process.once('SIGINT', () => { stop(0).catch(() => process.exit(1)); });
247
- process.once('SIGTERM', () => { stop(0).catch(() => process.exit(1)); });
248
- await client.connect();
249
- await engine.start();
250
- const doReload = async (trigger) => {
251
- try {
252
- const fresh = loadAutomation(pathArg);
253
- if (!fresh)
254
- return;
255
- const result = await engine.reload(fresh.automation, fresh.aliases);
256
- if (result.changed) {
257
- if (!isJsonMode()) {
258
- console.error(`rules: reloaded (${trigger}) — ${engine.getStats().rulesActive} active rule(s)`);
259
- for (const w of result.warnings)
260
- console.error(` warning: ${w}`);
261
- }
262
- else {
263
- printJson({
264
- kind: 'control',
265
- controlKind: 'reloaded',
266
- t: new Date().toISOString(),
267
- trigger,
268
- rulesActive: engine.getStats().rulesActive,
269
- warnings: result.warnings,
270
- });
271
- }
272
- }
273
- else {
274
- const msg = `rules: reload refused — ${result.errors.join(', ')}`;
275
- if (!isJsonMode())
276
- console.error(msg);
277
- else
278
- printJson({ kind: 'control', controlKind: 'reload-refused', errors: result.errors });
279
- }
280
- }
281
- catch (err) {
282
- const msg = `rules: reload failed — ${err instanceof Error ? err.message : String(err)}`;
283
- if (!isJsonMode())
284
- console.error(msg);
285
- else
286
- printJson({ kind: 'control', controlKind: 'reload-failed', error: msg });
287
- }
288
- };
289
- if (sighupSupported()) {
290
- process.on('SIGHUP', () => { doReload('signal').catch(() => undefined); });
291
- }
292
- const reloadPoll = setInterval(() => {
293
- if (consumeReloadSentinel(pidPaths.reloadFile)) {
294
- doReload('sentinel').catch(() => undefined);
295
- }
296
- }, 2000);
297
- reloadPoll.unref();
298
- if (!isJsonMode()) {
299
- console.error(`Rules engine started — ${engine.getStats().rulesActive} active rule(s), ${opts.dryRun ? 'global dry-run' : 'live'}.`);
300
- console.error(`pid ${process.pid} (${pidPaths.pidFile}); reload: \`switchbot rules reload\`.`);
301
- if (needsWebhook) {
302
- const boundPort = engine.getWebhookPort();
303
- console.error(`Webhook listener on ${opts.webhookHost ?? '127.0.0.1'}:${boundPort ?? '?'} (bearer file: ${webhookTokenStore.getFilePath()}).`);
304
- }
305
- }
306
- else {
307
- printJson({
308
- kind: 'control',
309
- controlKind: 'session_start',
310
- t: new Date().toISOString(),
311
- pid: process.pid,
312
- pidFile: pidPaths.pidFile,
313
- rulesActive: engine.getStats().rulesActive,
314
- globalDryRun: opts.dryRun === true,
315
- webhookPort: needsWebhook ? engine.getWebhookPort() : null,
316
- });
317
- }
318
- // Keep the process alive until SIGINT/SIGTERM or maxFirings stops the
319
- // engine. Poll the engine state rather than blocking forever — a
320
- // long-running process with zero wake-ups is still cheap.
321
- await new Promise((resolve) => {
322
- const tick = setInterval(() => {
323
- const s = engine.getStats();
324
- if (!s.started) {
325
- clearInterval(tick);
326
- clearInterval(reloadPoll);
327
- resolve();
328
- }
329
- }, 1000);
330
- });
331
- await stop(0);
332
- });
333
- }
334
- function resolveSinceMs(since) {
335
- if (since === undefined)
336
- return undefined;
337
- const durMs = parseDurationToMs(since);
338
- if (durMs === null) {
339
- exitWithError({
340
- code: 2,
341
- kind: 'usage',
342
- message: `Invalid --since value "${since}". Expected e.g. "30s", "15m", "1h", "7d".`,
343
- extra: { subKind: 'invalid-since' },
344
- });
345
- }
346
- return Date.now() - durMs;
347
- }
348
- function formatAuditLine(e) {
349
- const rule = e.rule?.name ?? '(no-rule)';
350
- const trigger = e.rule?.triggerSource ?? '?';
351
- const device = e.rule?.matchedDevice ?? e.deviceId ?? '-';
352
- const status = e.kind === 'rule-fire'
353
- ? e.result === 'error'
354
- ? 'error'
355
- : 'fire'
356
- : e.kind === 'rule-fire-dry'
357
- ? 'dry'
358
- : e.kind === 'rule-throttled'
359
- ? 'throttled'
360
- : 'rejected';
361
- const reason = e.rule?.reason ?? e.error ?? '';
362
- const reasonSuffix = reason ? ` ${reason}` : '';
363
- return `${e.t} ${status.padEnd(9)} ${rule} [${trigger}:${device}]${reasonSuffix}`;
364
- }
365
- function registerTail(rules) {
366
- rules
367
- .command('tail')
368
- .description('Stream rule-* entries from the audit log.')
369
- .option('--file <path>', `Audit log path (default ${DEFAULT_AUDIT_PATH})`)
370
- .option('--since <duration>', 'Only entries newer than this window (e.g. 1h, 30m, 7d).')
371
- .option('--rule <name>', 'Filter to a single rule name.')
372
- .option('-f, --follow', 'Keep the process open and stream new lines as they arrive.')
373
- .action(async (opts) => {
374
- const file = opts.file ?? DEFAULT_AUDIT_PATH;
375
- const sinceMs = resolveSinceMs(opts.since);
376
- const existing = fs.existsSync(file) ? readAudit(file) : [];
377
- const filtered = filterRuleAudits(existing, { sinceMs, ruleName: opts.rule });
378
- if (isJsonMode()) {
379
- for (const e of filtered)
380
- console.log(JSON.stringify(e));
381
- }
382
- else if (filtered.length === 0 && !opts.follow) {
383
- console.log(`(no rule-* entries in ${file}${opts.rule ? ` for rule "${opts.rule}"` : ''})`);
384
- }
385
- else {
386
- for (const e of filtered)
387
- console.log(formatAuditLine(e));
388
- }
389
- if (!opts.follow)
390
- return;
391
- // Follow: poll the file size and parse only newly appended bytes.
392
- // Audit writes are append-only and infrequent, so 500 ms is plenty.
393
- let offset = fs.existsSync(file) ? fs.statSync(file).size : 0;
394
- let buffer = '';
395
- const emit = (line) => {
396
- const trimmed = line.trim();
397
- if (!trimmed)
398
- return;
399
- let entry;
400
- try {
401
- entry = JSON.parse(trimmed);
402
- }
403
- catch {
404
- return;
405
- }
406
- const kept = filterRuleAudits([entry], { sinceMs, ruleName: opts.rule });
407
- if (kept.length === 0)
408
- return;
409
- if (isJsonMode())
410
- console.log(JSON.stringify(entry));
411
- else
412
- console.log(formatAuditLine(entry));
413
- };
414
- const poll = setInterval(() => {
415
- if (!fs.existsSync(file))
416
- return;
417
- const size = fs.statSync(file).size;
418
- if (size < offset) {
419
- // Log was truncated / rotated — restart from the top.
420
- offset = 0;
421
- buffer = '';
422
- }
423
- if (size === offset)
424
- return;
425
- const fd = fs.openSync(file, 'r');
426
- try {
427
- const chunk = Buffer.alloc(size - offset);
428
- fs.readSync(fd, chunk, 0, chunk.length, offset);
429
- offset = size;
430
- buffer += chunk.toString('utf-8');
431
- }
432
- finally {
433
- fs.closeSync(fd);
434
- }
435
- let newline = buffer.indexOf('\n');
436
- while (newline !== -1) {
437
- emit(buffer.slice(0, newline));
438
- buffer = buffer.slice(newline + 1);
439
- newline = buffer.indexOf('\n');
440
- }
441
- }, 500);
442
- await new Promise((resolve) => {
443
- const onStop = () => {
444
- clearInterval(poll);
445
- resolve();
446
- };
447
- process.once('SIGINT', onStop);
448
- process.once('SIGTERM', onStop);
449
- });
450
- });
451
- }
452
- function formatReplayTable(report) {
453
- const lines = [];
454
- lines.push(`total rule-entries: ${report.total}`);
455
- if (report.webhookRejectedCount > 0) {
456
- lines.push(`webhook-rejected (no rule): ${report.webhookRejectedCount}`);
457
- }
458
- if (report.summaries.length === 0) {
459
- lines.push('(no rules recorded in the audit window)');
460
- return lines.join('\n');
461
- }
462
- lines.push('rule | trigger | fires | dries | throttled | errors | error% | first | last');
463
- for (const s of report.summaries) {
464
- lines.push(`${s.rule} | ${s.triggerSource ?? '-'} | ${s.fires} | ${s.driesFires} | ${s.throttled} | ${s.errors} | ${(s.errorRate * 100).toFixed(1)}% | ${s.firstAt ?? '-'} | ${s.lastAt ?? '-'}`);
465
- }
466
- return lines.join('\n');
467
- }
468
- function registerReplay(rules) {
469
- rules
470
- .command('replay')
471
- .description('Aggregate rule-* audit entries per rule (fire/throttle/error counts).')
472
- .option('--file <path>', `Audit log path (default ${DEFAULT_AUDIT_PATH})`)
473
- .option('--since <duration>', 'Only entries newer than this window (e.g. 1h, 7d).')
474
- .option('--rule <name>', 'Filter to a single rule name.')
475
- .action((opts) => {
476
- const file = opts.file ?? DEFAULT_AUDIT_PATH;
477
- const entries = fs.existsSync(file) ? readAudit(file) : [];
478
- const sinceMs = resolveSinceMs(opts.since);
479
- const filtered = filterRuleAudits(entries, {
480
- sinceMs,
481
- ruleName: opts.rule,
482
- kinds: RULE_AUDIT_KINDS,
483
- });
484
- const report = aggregateRuleAudits(filtered);
485
- if (isJsonMode()) {
486
- printJson({
487
- file,
488
- sinceMs: sinceMs ?? null,
489
- ruleFilter: opts.rule ?? null,
490
- ...report,
491
- });
492
- }
493
- else {
494
- console.log(formatReplayTable(report));
495
- }
496
- });
497
- }
498
- function registerReload(rules) {
499
- rules
500
- .command('reload')
501
- .description('Trigger a policy hot-reload on the running `rules run` process.')
502
- .action(() => {
503
- const pidPaths = getDefaultPidFilePaths();
504
- const pid = readPidFile(pidPaths.pidFile);
505
- if (pid === null || !isPidAlive(pid)) {
506
- exitWithError({
507
- code: 2,
508
- kind: 'usage',
509
- message: `no running rules engine (pid file: ${pidPaths.pidFile}).`,
510
- extra: { subKind: 'no-engine', pidFile: pidPaths.pidFile },
511
- });
512
- }
513
- if (sighupSupported()) {
514
- try {
515
- process.kill(pid, 'SIGHUP');
516
- }
517
- catch (err) {
518
- exitWithError({
519
- code: 1,
520
- kind: 'runtime',
521
- message: `failed to send SIGHUP to pid ${pid}: ${err instanceof Error ? err.message : String(err)}`,
522
- extra: { subKind: 'signal-failed', pid },
523
- });
524
- }
525
- if (isJsonMode()) {
526
- printJson({ status: 'signalled', pid, method: 'SIGHUP' });
527
- }
528
- else {
529
- console.log(`Sent SIGHUP to pid ${pid}.`);
530
- }
531
- }
532
- else {
533
- writeReloadSentinel(pidPaths.reloadFile);
534
- if (isJsonMode()) {
535
- printJson({
536
- status: 'signalled',
537
- pid,
538
- method: 'sentinel',
539
- file: pidPaths.reloadFile,
540
- });
541
- }
542
- else {
543
- console.log(`Wrote reload sentinel ${pidPaths.reloadFile}; engine polls every 2 s.`);
544
- }
545
- }
546
- });
547
- }
548
- function registerWebhookRotateToken(rules) {
549
- rules
550
- .command('webhook-rotate-token')
551
- .description('Generate and persist a fresh webhook bearer token.')
552
- .action(() => {
553
- const store = new WebhookTokenStore();
554
- const fresh = store.rotate();
555
- if (isJsonMode()) {
556
- printJson({ status: 'rotated', filePath: store.getFilePath(), tokenLength: fresh.length });
557
- }
558
- else {
559
- console.log(`Webhook bearer rotated. Token written to ${store.getFilePath()}.`);
560
- console.log('New token (copy now — it is not shown again):');
561
- console.log(fresh);
562
- }
563
- });
564
- }
565
- function registerWebhookShowToken(rules) {
566
- rules
567
- .command('webhook-show-token')
568
- .description('Print the current webhook bearer token (creating one if absent).')
569
- .action(() => {
570
- const store = new WebhookTokenStore();
571
- const token = store.getOrCreate();
572
- if (isJsonMode()) {
573
- printJson({ filePath: store.getFilePath(), tokenLength: token.length });
574
- }
575
- else {
576
- console.log(token);
577
- }
578
- });
579
- }
580
- function registerSuggest(rules) {
581
- rules
582
- .command('suggest')
583
- .description('Generate a candidate rule YAML from intent + devices (heuristic, no LLM)')
584
- .requiredOption('--intent <text>', 'Natural language description of the automation')
585
- .option('--trigger <type>', 'mqtt | cron | webhook (inferred from intent if omitted)')
586
- .option('--device <id>', 'Device ID or alias to include (repeatable)', (v, prev) => [...prev, v], [])
587
- .option('--event <type>', 'MQTT event name override (e.g. motion.detected)')
588
- .option('--schedule <cron>', '5-field cron expression override')
589
- .option('--days <days>', 'Weekday filter, comma-separated (e.g. mon,tue,wed,thu,fri)')
590
- .option('--webhook-path <path>', 'Webhook path override (default: /action)')
591
- .option('--out <file>', 'Write YAML to file instead of stdout')
592
- .action((opts) => {
593
- const trigger = opts.trigger;
594
- const days = opts.days ? opts.days.split(',').map((d) => d.trim()) : undefined;
595
- const devices = opts.device.map((ref) => {
596
- const cached = getCachedDevice(ref);
597
- return { id: ref, name: cached?.name, type: cached?.type };
598
- });
599
- const { rule, ruleYaml, warnings } = suggestRule({
600
- intent: opts.intent,
601
- trigger,
602
- devices,
603
- event: opts.event,
604
- schedule: opts.schedule,
605
- days,
606
- webhookPath: opts.webhookPath,
607
- });
608
- for (const w of warnings)
609
- process.stderr.write(`warning: ${w}\n`);
610
- if (opts.out) {
611
- fs.writeFileSync(opts.out, ruleYaml, 'utf8');
612
- if (!isJsonMode())
613
- console.log(`✓ rule YAML written to ${opts.out}`);
614
- }
615
- else if (isJsonMode()) {
616
- printJson({ rule, rule_yaml: ruleYaml, warnings });
617
- }
618
- else {
619
- process.stdout.write(ruleYaml);
620
- }
621
- });
622
- }
623
- function formatConflictReport(report) {
624
- const lines = [];
625
- lines.push(`findings: ${report.findings.length} errors: ${report.counts.error} warnings: ${report.counts.warning} info: ${report.counts.info}`);
626
- if (report.findings.length === 0) {
627
- lines.push('No conflicts detected.');
628
- return lines.join('\n');
629
- }
630
- for (const f of report.findings) {
631
- lines.push(` [${f.severity}] ${f.code}: ${f.message}`);
632
- if (f.hint)
633
- lines.push(` hint: ${f.hint}`);
634
- }
635
- return lines.join('\n');
636
- }
637
- function registerConflicts(rules) {
638
- rules
639
- .command('conflicts [path]')
640
- .description('Detect conflicting or risky rule patterns (opposing actions, high-frequency catch-all, destructive commands).')
641
- .action((pathArg) => {
642
- const loaded = loadAutomation(pathArg);
643
- if (!loaded)
644
- return;
645
- const allRules = loaded.automation?.rules ?? [];
646
- const report = analyzeConflicts(allRules, loaded.quietHours);
647
- if (isJsonMode()) {
648
- printJson({
649
- policyPath: loaded.path,
650
- ruleCount: allRules.length,
651
- ...report,
652
- });
653
- }
654
- else {
655
- console.log(formatConflictReport(report));
656
- }
657
- process.exit(report.clean ? 0 : 1);
658
- });
659
- }
660
- function registerDoctor(rules) {
661
- rules
662
- .command('doctor [path]')
663
- .description('Combined health check: lint + conflict analysis + operational guidance.')
664
- .action((pathArg) => {
665
- const loaded = loadAutomation(pathArg);
666
- if (!loaded)
667
- return;
668
- const allRules = loaded.automation?.rules ?? [];
669
- const lintResult = lintRules(loaded.automation);
670
- const conflictReport = analyzeConflicts(allRules, loaded.quietHours);
671
- const overall = lintResult.valid && conflictReport.clean;
672
- if (isJsonMode()) {
673
- printJson({
674
- policyPath: loaded.path,
675
- policySchemaVersion: loaded.schemaVersion,
676
- automationEnabled: loaded.automation?.enabled === true,
677
- overall,
678
- lint: lintResult,
679
- conflicts: conflictReport,
680
- });
681
- }
682
- else {
683
- console.log('=== Lint ===');
684
- console.log(formatLintHuman(lintResult, loaded.schemaVersion));
685
- console.log('\n=== Conflicts ===');
686
- console.log(formatConflictReport(conflictReport));
687
- console.log(`\noverall: ${overall ? 'ok' : 'issues found'}`);
688
- }
689
- process.exit(overall ? 0 : 1);
690
- });
691
- }
692
- function registerSummary(rules) {
693
- rules
694
- .command('summary')
695
- .description('Aggregate rule-* audit entries per rule over a time window (fires, throttled, errors).')
696
- .option('--file <path>', `Audit log path (default ${DEFAULT_AUDIT_PATH})`)
697
- .option('--since <duration>', 'Only entries newer than this window (default: 24h). E.g. 1h, 7d.')
698
- .option('--rule <name>', 'Filter to a single rule name.')
699
- .action((opts) => {
700
- const file = opts.file ?? DEFAULT_AUDIT_PATH;
701
- const entries = fs.existsSync(file) ? readAudit(file) : [];
702
- const sinceMs = resolveSinceMs(opts.since ?? '24h');
703
- const filtered = filterRuleAudits(entries, { sinceMs, ruleName: opts.rule });
704
- const report = aggregateRuleAudits(filtered);
705
- if (isJsonMode()) {
706
- printJson({ file, window: opts.since ?? '24h', ruleFilter: opts.rule ?? null, ...report });
707
- return;
708
- }
709
- console.log(`Rule summary (${opts.since ?? '24h'} window, ${report.total} entries)`);
710
- if (report.summaries.length === 0) {
711
- console.log('(no rule activity in this window)');
712
- return;
713
- }
714
- printTable(['Rule', 'Trigger', 'Fires', 'Throttled', 'Errors', 'Error%', 'Last fired'], report.summaries.map((s) => [
715
- s.rule,
716
- s.triggerSource ?? '-',
717
- String(s.fires),
718
- String(s.throttled),
719
- String(s.errors),
720
- `${(s.errorRate * 100).toFixed(1)}%`,
721
- s.lastAt ?? '-',
722
- ]));
723
- });
724
- }
725
- function registerLastFired(rules) {
726
- rules
727
- .command('last-fired')
728
- .description('Show the N most recently fired rule-fire entries from the audit log.')
729
- .option('--file <path>', `Audit log path (default ${DEFAULT_AUDIT_PATH})`)
730
- .option('--rule <name>', 'Filter to a single rule name.')
731
- .option('-n <count>', 'Number of entries to show (default: 10).', (v) => Number.parseInt(v, 10))
732
- .action((opts) => {
733
- const file = opts.file ?? DEFAULT_AUDIT_PATH;
734
- const n = opts.n ?? 10;
735
- const entries = fs.existsSync(file) ? readAudit(file) : [];
736
- const fires = filterRuleAudits(entries, {
737
- ruleName: opts.rule,
738
- kinds: ['rule-fire', 'rule-fire-dry'],
739
- });
740
- const recent = fires.slice(-n).reverse();
741
- if (isJsonMode()) {
742
- printJson({ file, ruleFilter: opts.rule ?? null, count: recent.length, entries: recent });
743
- return;
744
- }
745
- if (recent.length === 0) {
746
- console.log(`(no rule-fire entries in ${file}${opts.rule ? ` for rule "${opts.rule}"` : ''})`);
747
- return;
748
- }
749
- for (const e of recent) {
750
- const parts = [e.t, e.kind, e.rule?.name ?? '-'];
751
- if (e.deviceId)
752
- parts.push(`device=${e.deviceId}`);
753
- if (e.command)
754
- parts.push(`cmd=${e.command}`);
755
- if (e.result)
756
- parts.push(`result=${e.result}`);
757
- console.log(parts.join(' '));
758
- }
759
- });
760
- }
761
- function registerExplain(rules) {
762
- rules
763
- .command('explain <name> [path]')
764
- .description('Show full detail for a named rule: trigger, conditions, actions, and last-fired time.')
765
- .option('--file <path>', `Audit log path (default ${DEFAULT_AUDIT_PATH}).`)
766
- .action((name, pathArg, opts) => {
767
- const loaded = loadAutomation(pathArg);
768
- if (!loaded)
769
- return;
770
- const allRules = loaded.automation?.rules ?? [];
771
- const rule = allRules.find((r) => r.name === name);
772
- if (!rule) {
773
- exitWithError({
774
- code: 1,
775
- kind: 'usage',
776
- message: `Rule "${name}" not found in policy.`,
777
- extra: {
778
- subKind: 'rule-not-found',
779
- available: allRules.map((r) => r.name),
780
- },
781
- });
782
- return;
783
- }
784
- const auditFile = opts.file ?? DEFAULT_AUDIT_PATH;
785
- const entries = fs.existsSync(auditFile) ? readAudit(auditFile) : [];
786
- const fires = filterRuleAudits(entries, { ruleName: name, kinds: ['rule-fire', 'rule-fire-dry'] });
787
- const lastFired = fires.length > 0 ? fires[fires.length - 1].t : null;
788
- const detail = {
789
- name: rule.name,
790
- enabled: rule.enabled !== false,
791
- trigger: describeTrigger(rule),
792
- conditions: rule.conditions ?? [],
793
- actions: rule.then,
794
- cooldown: rule.cooldown ?? rule.throttle?.max_per ?? null,
795
- hysteresis: rule.hysteresis ?? rule.requires_stable_for ?? null,
796
- maxFiringsPerHour: rule.maxFiringsPerHour ?? null,
797
- suppressIfAlreadyDesired: rule.suppressIfAlreadyDesired ?? false,
798
- dryRun: rule.dry_run === true,
799
- lastFired,
800
- };
801
- if (isJsonMode()) {
802
- printJson(detail);
803
- return;
804
- }
805
- console.log(`name: ${detail.name}`);
806
- console.log(`enabled: ${detail.enabled}`);
807
- console.log(`trigger: ${detail.trigger}`);
808
- console.log(`conditions: ${detail.conditions.length === 0 ? '(none)' : JSON.stringify(detail.conditions)}`);
809
- console.log(`actions: ${detail.actions.length}`);
810
- for (const a of detail.actions) {
811
- console.log(` - ${a.command}${a.device ? ` [${a.device}]` : ''}${a.on_error ? ` on_error=${a.on_error}` : ''}`);
812
- }
813
- if (detail.cooldown)
814
- console.log(`cooldown: ${detail.cooldown}`);
815
- if (detail.hysteresis)
816
- console.log(`hysteresis: ${detail.hysteresis}`);
817
- if (detail.maxFiringsPerHour !== null)
818
- console.log(`maxFiringsPerHour: ${detail.maxFiringsPerHour}`);
819
- if (detail.suppressIfAlreadyDesired)
820
- console.log(`suppressIfAlreadyDesired: true`);
821
- if (detail.dryRun)
822
- console.log(`dry_run: true`);
823
- console.log(`last fired: ${detail.lastFired ?? '(never)'}`);
824
- });
825
- }
826
- export function registerRulesCommand(program) {
827
- const rules = program
828
- .command('rules')
829
- .description('Run, list, and lint automation rules declared in policy.yaml (v0.2, preview).')
830
- .addHelpText('after', `
831
- Reads the same policy file as \`switchbot policy\` (${DEFAULT_POLICY_PATH} by
832
- default; override with --policy or $SWITCHBOT_POLICY_PATH).
833
-
834
- Subcommands:
835
- suggest Generate a candidate rule YAML from intent (heuristic, no LLM).
836
- lint [path] Static-check rule definitions; no MQTT, no API calls.
837
- list [path] Print a human/JSON summary of each rule's trigger + actions.
838
- explain <name> Show full detail for a rule: trigger, conditions, actions, last-fired.
839
- run [path] Subscribe to MQTT (+ cron/webhook) and execute matching rules.
840
- reload Hot-reload the running engine's policy (SIGHUP on Unix,
841
- pid-file sentinel on Windows).
842
- tail Stream rule-* entries from the audit log (--follow tails).
843
- replay Per-rule aggregate: fires/dries/throttled/errors + window.
844
- conflicts [path] Detect conflicting or risky rule patterns.
845
- doctor [path] Combined health check: lint + conflict analysis + summary.
846
- summary Aggregate rule-fire counts per rule over a time window.
847
- last-fired Show the N most recently fired rule-fire audit entries.
848
- webhook-rotate-token Rotate the bearer token used for webhook triggers.
849
- webhook-show-token Print the current bearer token (creating one if absent).
850
-
851
- MQTT, cron, and webhook triggers are all wired. Destructive commands (lock /
852
- unlock / deleteWebhook / deleteScene / factoryReset) are rejected at lint.
853
-
854
- Exit codes (lint):
855
- 0 valid
856
- 1 one or more rules have errors
857
- 2 policy file not found
858
- 3 YAML parse error
859
- 4 internal / schema validation failed
860
- `);
861
- registerSuggest(rules);
862
- registerLint(rules);
863
- registerList(rules);
864
- registerExplain(rules);
865
- registerRun(rules);
866
- registerReload(rules);
867
- registerTail(rules);
868
- registerReplay(rules);
869
- registerConflicts(rules);
870
- registerDoctor(rules);
871
- registerSummary(rules);
872
- registerLastFired(rules);
873
- registerWebhookRotateToken(rules);
874
- registerWebhookShowToken(rules);
875
- }
876
- //# sourceMappingURL=rules.js.map