@switchbot/openapi-cli 3.2.0 → 3.2.1
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/dist/api/client.d.ts +31 -0
- package/dist/api/client.js +236 -0
- package/dist/api/client.js.map +1 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +21 -0
- package/dist/auth.js.map +1 -0
- package/dist/commands/agent-bootstrap.d.ts +10 -0
- package/dist/commands/agent-bootstrap.js +200 -0
- package/dist/commands/agent-bootstrap.js.map +1 -0
- package/dist/commands/auth.d.ts +18 -0
- package/dist/commands/auth.js +355 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +414 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/cache.d.ts +2 -0
- package/dist/commands/cache.js +127 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/capabilities.d.ts +31 -0
- package/dist/commands/capabilities.js +383 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/catalog.d.ts +2 -0
- package/dist/commands/catalog.js +360 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +386 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/config.d.ts +21 -0
- package/dist/commands/config.js +377 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +411 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/device-meta.d.ts +2 -0
- package/dist/commands/device-meta.js +160 -0
- package/dist/commands/device-meta.js.map +1 -0
- package/dist/commands/devices.d.ts +2 -0
- package/dist/commands/devices.js +949 -0
- package/dist/commands/devices.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +1016 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.d.ts +31 -0
- package/dist/commands/events.js +564 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/expand.d.ts +2 -0
- package/dist/commands/expand.js +131 -0
- package/dist/commands/expand.js.map +1 -0
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +140 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/health.d.ts +8 -0
- package/dist/commands/health.js +114 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +321 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/identity.d.ts +45 -0
- package/dist/commands/identity.js +60 -0
- package/dist/commands/identity.js.map +1 -0
- package/dist/commands/install.d.ts +20 -0
- package/dist/commands/install.js +247 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/mcp.d.ts +14 -0
- package/dist/commands/mcp.js +2018 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/plan.d.ts +51 -0
- package/dist/commands/plan.js +654 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/policy.d.ts +24 -0
- package/dist/commands/policy.js +587 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/quota.d.ts +2 -0
- package/dist/commands/quota.js +79 -0
- package/dist/commands/quota.js.map +1 -0
- package/dist/commands/rules.d.ts +2 -0
- package/dist/commands/rules.js +876 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/commands/scenes.d.ts +2 -0
- package/dist/commands/scenes.js +265 -0
- package/dist/commands/scenes.js.map +1 -0
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/schema.js +185 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/status-sync.d.ts +2 -0
- package/dist/commands/status-sync.js +132 -0
- package/dist/commands/status-sync.js.map +1 -0
- package/dist/commands/uninstall.d.ts +20 -0
- package/dist/commands/uninstall.js +238 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts +2 -0
- package/dist/commands/upgrade-check.js +107 -0
- package/dist/commands/upgrade-check.js.map +1 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +195 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/webhook.d.ts +2 -0
- package/dist/commands/webhook.js +183 -0
- package/dist/commands/webhook.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +259 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials/backends/file.d.ts +18 -0
- package/dist/credentials/backends/file.js +102 -0
- package/dist/credentials/backends/file.js.map +1 -0
- package/dist/credentials/backends/linux.d.ts +16 -0
- package/dist/credentials/backends/linux.js +130 -0
- package/dist/credentials/backends/linux.js.map +1 -0
- package/dist/credentials/backends/macos.d.ts +18 -0
- package/dist/credentials/backends/macos.js +130 -0
- package/dist/credentials/backends/macos.js.map +1 -0
- package/dist/credentials/backends/windows.d.ts +23 -0
- package/dist/credentials/backends/windows.js +216 -0
- package/dist/credentials/backends/windows.js.map +1 -0
- package/dist/credentials/keychain.d.ts +83 -0
- package/dist/credentials/keychain.js +89 -0
- package/dist/credentials/keychain.js.map +1 -0
- package/dist/credentials/prime.d.ts +32 -0
- package/dist/credentials/prime.js +53 -0
- package/dist/credentials/prime.js.map +1 -0
- package/dist/devices/cache.d.ts +79 -0
- package/dist/devices/cache.js +294 -0
- package/dist/devices/cache.js.map +1 -0
- package/dist/devices/catalog.d.ts +138 -0
- package/dist/devices/catalog.js +768 -0
- package/dist/devices/catalog.js.map +1 -0
- package/dist/devices/device-meta.d.ts +15 -0
- package/dist/devices/device-meta.js +57 -0
- package/dist/devices/device-meta.js.map +1 -0
- package/dist/devices/history-agg.d.ts +37 -0
- package/dist/devices/history-agg.js +139 -0
- package/dist/devices/history-agg.js.map +1 -0
- package/dist/devices/history-query.d.ts +45 -0
- package/dist/devices/history-query.js +182 -0
- package/dist/devices/history-query.js.map +1 -0
- package/dist/devices/param-validator.d.ts +40 -0
- package/dist/devices/param-validator.js +434 -0
- package/dist/devices/param-validator.js.map +1 -0
- package/dist/devices/resources.d.ts +74 -0
- package/dist/devices/resources.js +271 -0
- package/dist/devices/resources.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +170 -56946
- package/dist/index.js.map +1 -0
- package/dist/install/default-steps.d.ts +66 -0
- package/dist/install/default-steps.js +258 -0
- package/dist/install/default-steps.js.map +1 -0
- package/dist/install/preflight.d.ts +60 -0
- package/dist/install/preflight.js +213 -0
- package/dist/install/preflight.js.map +1 -0
- package/dist/install/steps.d.ts +61 -0
- package/dist/install/steps.js +68 -0
- package/dist/install/steps.js.map +1 -0
- package/dist/lib/command-keywords.d.ts +5 -0
- package/dist/lib/command-keywords.js +18 -0
- package/dist/lib/command-keywords.js.map +1 -0
- package/dist/lib/daemon-state.d.ts +24 -0
- package/dist/lib/daemon-state.js +47 -0
- package/dist/lib/daemon-state.js.map +1 -0
- package/dist/lib/destructive-mode.d.ts +2 -0
- package/dist/lib/destructive-mode.js +13 -0
- package/dist/lib/destructive-mode.js.map +1 -0
- package/dist/lib/devices.d.ts +151 -0
- package/dist/lib/devices.js +383 -0
- package/dist/lib/devices.js.map +1 -0
- package/dist/lib/idempotency.d.ts +46 -0
- package/dist/lib/idempotency.js +107 -0
- package/dist/lib/idempotency.js.map +1 -0
- package/dist/lib/plan-store.d.ts +19 -0
- package/dist/lib/plan-store.js +69 -0
- package/dist/lib/plan-store.js.map +1 -0
- package/dist/lib/request-context.d.ts +7 -0
- package/dist/lib/request-context.js +13 -0
- package/dist/lib/request-context.js.map +1 -0
- package/dist/lib/scenes.d.ts +7 -0
- package/dist/lib/scenes.js +11 -0
- package/dist/lib/scenes.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +17 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/device-history.d.ts +36 -0
- package/dist/mcp/device-history.js +146 -0
- package/dist/mcp/device-history.js.map +1 -0
- package/dist/mcp/events-subscription.d.ts +45 -0
- package/dist/mcp/events-subscription.js +214 -0
- package/dist/mcp/events-subscription.js.map +1 -0
- package/dist/mqtt/client.d.ts +25 -0
- package/dist/mqtt/client.js +181 -0
- package/dist/mqtt/client.js.map +1 -0
- package/dist/mqtt/credential.d.ts +16 -0
- package/dist/mqtt/credential.js +31 -0
- package/dist/mqtt/credential.js.map +1 -0
- package/dist/policy/add-rule.d.ts +21 -0
- package/dist/policy/add-rule.js +125 -0
- package/dist/policy/add-rule.js.map +1 -0
- package/dist/policy/diff.d.ts +21 -0
- package/dist/policy/diff.js +92 -0
- package/dist/policy/diff.js.map +1 -0
- package/dist/policy/format.d.ts +6 -0
- package/dist/policy/format.js +58 -0
- package/dist/policy/format.js.map +1 -0
- package/dist/policy/load.d.ts +32 -0
- package/dist/policy/load.js +62 -0
- package/dist/policy/load.js.map +1 -0
- package/dist/policy/migrate.d.ts +21 -0
- package/dist/policy/migrate.js +68 -0
- package/dist/policy/migrate.js.map +1 -0
- package/dist/policy/schema.d.ts +5 -0
- package/dist/policy/schema.js +19 -0
- package/dist/policy/schema.js.map +1 -0
- package/dist/policy/validate.d.ts +19 -0
- package/dist/policy/validate.js +263 -0
- package/dist/policy/validate.js.map +1 -0
- package/dist/rules/action.d.ts +65 -0
- package/dist/rules/action.js +217 -0
- package/dist/rules/action.js.map +1 -0
- package/dist/rules/audit-query.d.ts +51 -0
- package/dist/rules/audit-query.js +90 -0
- package/dist/rules/audit-query.js.map +1 -0
- package/dist/rules/conflict-analyzer.d.ts +57 -0
- package/dist/rules/conflict-analyzer.js +215 -0
- package/dist/rules/conflict-analyzer.js.map +1 -0
- package/dist/rules/cron-scheduler.d.ts +62 -0
- package/dist/rules/cron-scheduler.js +187 -0
- package/dist/rules/cron-scheduler.js.map +1 -0
- package/dist/rules/destructive.d.ts +20 -0
- package/dist/rules/destructive.js +53 -0
- package/dist/rules/destructive.js.map +1 -0
- package/dist/rules/engine.d.ts +193 -0
- package/dist/rules/engine.js +758 -0
- package/dist/rules/engine.js.map +1 -0
- package/dist/rules/matcher.d.ts +56 -0
- package/dist/rules/matcher.js +231 -0
- package/dist/rules/matcher.js.map +1 -0
- package/dist/rules/pid-file.d.ts +43 -0
- package/dist/rules/pid-file.js +96 -0
- package/dist/rules/pid-file.js.map +1 -0
- package/dist/rules/quiet-hours.d.ts +26 -0
- package/dist/rules/quiet-hours.js +46 -0
- package/dist/rules/quiet-hours.js.map +1 -0
- package/dist/rules/suggest.d.ts +20 -0
- package/dist/rules/suggest.js +96 -0
- package/dist/rules/suggest.js.map +1 -0
- package/dist/rules/throttle.d.ts +61 -0
- package/dist/rules/throttle.js +117 -0
- package/dist/rules/throttle.js.map +1 -0
- package/dist/rules/types.d.ts +117 -0
- package/dist/rules/types.js +35 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/webhook-listener.d.ts +63 -0
- package/dist/rules/webhook-listener.js +224 -0
- package/dist/rules/webhook-listener.js.map +1 -0
- package/dist/rules/webhook-token.d.ts +50 -0
- package/dist/rules/webhook-token.js +91 -0
- package/dist/rules/webhook-token.js.map +1 -0
- package/dist/schema/field-aliases.d.ts +34 -0
- package/dist/schema/field-aliases.js +132 -0
- package/dist/schema/field-aliases.js.map +1 -0
- package/dist/sinks/dispatcher.d.ts +7 -0
- package/dist/sinks/dispatcher.js +13 -0
- package/dist/sinks/dispatcher.js.map +1 -0
- package/dist/sinks/file.d.ts +6 -0
- package/dist/sinks/file.js +20 -0
- package/dist/sinks/file.js.map +1 -0
- package/dist/sinks/format.d.ts +20 -0
- package/dist/sinks/format.js +57 -0
- package/dist/sinks/format.js.map +1 -0
- package/dist/sinks/homeassistant.d.ts +18 -0
- package/dist/sinks/homeassistant.js +45 -0
- package/dist/sinks/homeassistant.js.map +1 -0
- package/dist/sinks/openclaw.d.ts +13 -0
- package/dist/sinks/openclaw.js +34 -0
- package/dist/sinks/openclaw.js.map +1 -0
- package/dist/sinks/stdout.d.ts +4 -0
- package/dist/sinks/stdout.js +6 -0
- package/dist/sinks/stdout.js.map +1 -0
- package/dist/sinks/telegram.d.ts +11 -0
- package/dist/sinks/telegram.js +29 -0
- package/dist/sinks/telegram.js.map +1 -0
- package/dist/sinks/types.d.ts +13 -0
- package/dist/sinks/types.js +2 -0
- package/dist/sinks/types.js.map +1 -0
- package/dist/sinks/webhook.d.ts +6 -0
- package/dist/sinks/webhook.js +23 -0
- package/dist/sinks/webhook.js.map +1 -0
- package/dist/status-sync/manager.d.ts +48 -0
- package/dist/status-sync/manager.js +269 -0
- package/dist/status-sync/manager.js.map +1 -0
- package/dist/utils/arg-parsers.d.ts +16 -0
- package/dist/utils/arg-parsers.js +67 -0
- package/dist/utils/arg-parsers.js.map +1 -0
- package/dist/utils/audit.d.ts +69 -0
- package/dist/utils/audit.js +122 -0
- package/dist/utils/audit.js.map +1 -0
- package/dist/utils/filter.d.ts +81 -0
- package/dist/utils/filter.js +190 -0
- package/dist/utils/filter.js.map +1 -0
- package/dist/utils/flags.d.ts +72 -0
- package/dist/utils/flags.js +187 -0
- package/dist/utils/flags.js.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +118 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/health.d.ts +48 -0
- package/dist/utils/health.js +102 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/help-json.d.ts +39 -0
- package/dist/utils/help-json.js +55 -0
- package/dist/utils/help-json.js.map +1 -0
- package/dist/utils/name-resolver.d.ts +26 -0
- package/dist/utils/name-resolver.js +138 -0
- package/dist/utils/name-resolver.js.map +1 -0
- package/dist/utils/output.d.ts +73 -0
- package/dist/utils/output.js +405 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/quota.d.ts +61 -0
- package/dist/utils/quota.js +228 -0
- package/dist/utils/quota.js.map +1 -0
- package/dist/utils/redact.d.ts +23 -0
- package/dist/utils/redact.js +69 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/retry.d.ts +72 -0
- package/dist/utils/retry.js +141 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/string.d.ts +2 -0
- package/dist/utils/string.js +23 -0
- package/dist/utils/string.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron trigger scheduler for the rules engine.
|
|
3
|
+
*
|
|
4
|
+
* Each cron rule gets its own scheduler entry. On every tick the
|
|
5
|
+
* scheduler synthesises an `EngineEvent` with `source: 'cron'` and hands
|
|
6
|
+
* it to the same dispatch path the MQTT pipeline uses, so conditions,
|
|
7
|
+
* throttle, and action execution behave identically regardless of
|
|
8
|
+
* trigger source.
|
|
9
|
+
*
|
|
10
|
+
* Tests can drive the scheduler deterministically via `fireNowForTest()`
|
|
11
|
+
* — the scheduler's internal timer still uses `setTimeout`, which means
|
|
12
|
+
* `vi.useFakeTimers()` plus `vi.advanceTimersByTime()` also work. Croner
|
|
13
|
+
* is used only for `nextRun(fromDate)` calculations; we own the
|
|
14
|
+
* timer/dispatch loop so the engine can drain events through a single
|
|
15
|
+
* serialised queue.
|
|
16
|
+
*/
|
|
17
|
+
import type { EngineEvent, Rule, DayOfWeek } from './types.js';
|
|
18
|
+
/** Return true if `t` falls on one of the listed days (or days is absent/empty). */
|
|
19
|
+
export declare function matchesDayFilter(days: DayOfWeek[] | undefined, t: Date): boolean;
|
|
20
|
+
export interface CronDispatch {
|
|
21
|
+
(rule: Rule, event: EngineEvent): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface CronSchedulerOptions {
|
|
24
|
+
/** Dispatch callback — the engine's queue wrapper that runs the rule. */
|
|
25
|
+
dispatch: CronDispatch;
|
|
26
|
+
/** Clock injection for tests; defaults to Date.now. */
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
}
|
|
29
|
+
export declare class CronScheduler {
|
|
30
|
+
private readonly opts;
|
|
31
|
+
private readonly entries;
|
|
32
|
+
private started;
|
|
33
|
+
private stopped;
|
|
34
|
+
constructor(opts: CronSchedulerOptions);
|
|
35
|
+
getScheduledFor(ruleName: string): {
|
|
36
|
+
schedule: string;
|
|
37
|
+
nextAt: Date | null;
|
|
38
|
+
} | null;
|
|
39
|
+
hasRegistered(ruleName: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Register a cron rule. Validates the pattern eagerly — an invalid
|
|
42
|
+
* schedule throws synchronously so engine start can surface the error.
|
|
43
|
+
*/
|
|
44
|
+
register(rule: Rule): void;
|
|
45
|
+
unregister(ruleName: string): void;
|
|
46
|
+
start(): void;
|
|
47
|
+
stop(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Test helper — compute the pattern's next run after a reference
|
|
50
|
+
* timestamp without actually scheduling it. Handy for regression tests.
|
|
51
|
+
*/
|
|
52
|
+
nextRunAfter(ruleName: string, after: Date): Date | null;
|
|
53
|
+
/**
|
|
54
|
+
* Test helper — fire a rule immediately, bypassing the timer. Used by
|
|
55
|
+
* unit tests to skip vi.advanceTimersByTime logic when the focus is on
|
|
56
|
+
* dispatch behaviour, not scheduling accuracy.
|
|
57
|
+
*/
|
|
58
|
+
fireNowForTest(ruleName: string): Promise<void>;
|
|
59
|
+
private nowDate;
|
|
60
|
+
private arm;
|
|
61
|
+
private fire;
|
|
62
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron trigger scheduler for the rules engine.
|
|
3
|
+
*
|
|
4
|
+
* Each cron rule gets its own scheduler entry. On every tick the
|
|
5
|
+
* scheduler synthesises an `EngineEvent` with `source: 'cron'` and hands
|
|
6
|
+
* it to the same dispatch path the MQTT pipeline uses, so conditions,
|
|
7
|
+
* throttle, and action execution behave identically regardless of
|
|
8
|
+
* trigger source.
|
|
9
|
+
*
|
|
10
|
+
* Tests can drive the scheduler deterministically via `fireNowForTest()`
|
|
11
|
+
* — the scheduler's internal timer still uses `setTimeout`, which means
|
|
12
|
+
* `vi.useFakeTimers()` plus `vi.advanceTimersByTime()` also work. Croner
|
|
13
|
+
* is used only for `nextRun(fromDate)` calculations; we own the
|
|
14
|
+
* timer/dispatch loop so the engine can drain events through a single
|
|
15
|
+
* serialised queue.
|
|
16
|
+
*/
|
|
17
|
+
import { Cron } from 'croner';
|
|
18
|
+
/** Maps JS getDay() (0=Sun) to 3-letter abbreviation. */
|
|
19
|
+
const JS_DAY_TO_ABBR = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
20
|
+
/** Expand a days[] entry to its canonical 3-letter abbr so comparisons are O(1). */
|
|
21
|
+
function normaliseDay(d) {
|
|
22
|
+
return d.toLowerCase().slice(0, 3);
|
|
23
|
+
}
|
|
24
|
+
/** Return true if `t` falls on one of the listed days (or days is absent/empty). */
|
|
25
|
+
export function matchesDayFilter(days, t) {
|
|
26
|
+
if (!days || days.length === 0)
|
|
27
|
+
return true;
|
|
28
|
+
const todayAbbr = JS_DAY_TO_ABBR[t.getDay()];
|
|
29
|
+
return days.some((d) => normaliseDay(d) === todayAbbr);
|
|
30
|
+
}
|
|
31
|
+
export class CronScheduler {
|
|
32
|
+
opts;
|
|
33
|
+
entries = new Map();
|
|
34
|
+
started = false;
|
|
35
|
+
stopped = false;
|
|
36
|
+
constructor(opts) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
}
|
|
39
|
+
getScheduledFor(ruleName) {
|
|
40
|
+
const s = this.entries.get(ruleName);
|
|
41
|
+
if (!s)
|
|
42
|
+
return null;
|
|
43
|
+
return { schedule: s.schedule, nextAt: s.nextAt };
|
|
44
|
+
}
|
|
45
|
+
hasRegistered(ruleName) {
|
|
46
|
+
return this.entries.has(ruleName);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register a cron rule. Validates the pattern eagerly — an invalid
|
|
50
|
+
* schedule throws synchronously so engine start can surface the error.
|
|
51
|
+
*/
|
|
52
|
+
register(rule) {
|
|
53
|
+
if (rule.when.source !== 'cron') {
|
|
54
|
+
throw new Error(`CronScheduler.register called for non-cron rule "${rule.name}"`);
|
|
55
|
+
}
|
|
56
|
+
if (this.entries.has(rule.name)) {
|
|
57
|
+
throw new Error(`CronScheduler: duplicate rule name "${rule.name}"`);
|
|
58
|
+
}
|
|
59
|
+
const schedule = rule.when.schedule;
|
|
60
|
+
let pattern;
|
|
61
|
+
try {
|
|
62
|
+
pattern = new Cron(schedule, { paused: true });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new Error(`CronScheduler: invalid cron expression for rule "${rule.name}": ${schedule} (${err instanceof Error ? err.message : String(err)})`);
|
|
66
|
+
}
|
|
67
|
+
const entry = {
|
|
68
|
+
rule,
|
|
69
|
+
schedule,
|
|
70
|
+
pattern,
|
|
71
|
+
timer: null,
|
|
72
|
+
nextAt: null,
|
|
73
|
+
};
|
|
74
|
+
this.entries.set(rule.name, entry);
|
|
75
|
+
if (this.started && !this.stopped)
|
|
76
|
+
this.arm(entry);
|
|
77
|
+
}
|
|
78
|
+
unregister(ruleName) {
|
|
79
|
+
const e = this.entries.get(ruleName);
|
|
80
|
+
if (!e)
|
|
81
|
+
return;
|
|
82
|
+
if (e.timer)
|
|
83
|
+
clearTimeout(e.timer);
|
|
84
|
+
try {
|
|
85
|
+
e.pattern.stop();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// croner throws when already stopped — ignore.
|
|
89
|
+
}
|
|
90
|
+
this.entries.delete(ruleName);
|
|
91
|
+
}
|
|
92
|
+
start() {
|
|
93
|
+
if (this.stopped) {
|
|
94
|
+
throw new Error('CronScheduler: cannot start after stop().');
|
|
95
|
+
}
|
|
96
|
+
if (this.started)
|
|
97
|
+
return;
|
|
98
|
+
this.started = true;
|
|
99
|
+
for (const entry of this.entries.values())
|
|
100
|
+
this.arm(entry);
|
|
101
|
+
}
|
|
102
|
+
stop() {
|
|
103
|
+
if (this.stopped)
|
|
104
|
+
return;
|
|
105
|
+
this.stopped = true;
|
|
106
|
+
this.started = false;
|
|
107
|
+
for (const e of this.entries.values()) {
|
|
108
|
+
if (e.timer)
|
|
109
|
+
clearTimeout(e.timer);
|
|
110
|
+
e.timer = null;
|
|
111
|
+
try {
|
|
112
|
+
e.pattern.stop();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Test helper — compute the pattern's next run after a reference
|
|
121
|
+
* timestamp without actually scheduling it. Handy for regression tests.
|
|
122
|
+
*/
|
|
123
|
+
nextRunAfter(ruleName, after) {
|
|
124
|
+
const e = this.entries.get(ruleName);
|
|
125
|
+
if (!e)
|
|
126
|
+
return null;
|
|
127
|
+
return e.pattern.nextRun(after) ?? null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Test helper — fire a rule immediately, bypassing the timer. Used by
|
|
131
|
+
* unit tests to skip vi.advanceTimersByTime logic when the focus is on
|
|
132
|
+
* dispatch behaviour, not scheduling accuracy.
|
|
133
|
+
*/
|
|
134
|
+
async fireNowForTest(ruleName) {
|
|
135
|
+
const e = this.entries.get(ruleName);
|
|
136
|
+
if (!e)
|
|
137
|
+
throw new Error(`CronScheduler.fireNowForTest: no rule "${ruleName}"`);
|
|
138
|
+
await this.fire(e);
|
|
139
|
+
}
|
|
140
|
+
nowDate() {
|
|
141
|
+
return this.opts.now ? this.opts.now() : new Date();
|
|
142
|
+
}
|
|
143
|
+
arm(entry) {
|
|
144
|
+
if (this.stopped)
|
|
145
|
+
return;
|
|
146
|
+
const now = this.nowDate();
|
|
147
|
+
const next = entry.pattern.nextRun(now);
|
|
148
|
+
if (!next) {
|
|
149
|
+
entry.nextAt = null;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
entry.nextAt = next;
|
|
153
|
+
const delayMs = Math.max(0, next.getTime() - now.getTime());
|
|
154
|
+
entry.timer = setTimeout(() => {
|
|
155
|
+
entry.timer = null;
|
|
156
|
+
// Fire and then re-arm, regardless of outcome — we never want one
|
|
157
|
+
// misbehaving rule to kill its own future ticks.
|
|
158
|
+
this.fire(entry)
|
|
159
|
+
.catch(() => undefined)
|
|
160
|
+
.finally(() => {
|
|
161
|
+
if (!this.stopped && this.entries.has(entry.rule.name))
|
|
162
|
+
this.arm(entry);
|
|
163
|
+
});
|
|
164
|
+
}, delayMs);
|
|
165
|
+
// Unref so a process with only cron rules still exits on SIGINT when
|
|
166
|
+
// the user expects (e.g. in integration tests).
|
|
167
|
+
if (typeof entry.timer.unref === 'function') {
|
|
168
|
+
entry.timer.unref();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async fire(entry) {
|
|
172
|
+
const when = this.nowDate();
|
|
173
|
+
// Apply the optional day-of-week filter before dispatching.
|
|
174
|
+
const trigger = entry.rule.when;
|
|
175
|
+
if (trigger.source === 'cron' && !matchesDayFilter(trigger.days, when)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const event = {
|
|
179
|
+
source: 'cron',
|
|
180
|
+
event: entry.schedule,
|
|
181
|
+
t: when,
|
|
182
|
+
payload: { schedule: entry.schedule },
|
|
183
|
+
};
|
|
184
|
+
await this.opts.dispatch(entry.rule, event);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=cron-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-scheduler.js","sourceRoot":"","sources":["../../src/rules/cron-scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive command parsing — single source of truth shared between the
|
|
3
|
+
* policy validator post-hook (rejects destructive commands inside
|
|
4
|
+
* `automation.rules[].then[].command`) and the runtime executor (second-
|
|
5
|
+
* line guard that refuses to shell out even if validation was bypassed).
|
|
6
|
+
*/
|
|
7
|
+
export declare const DESTRUCTIVE_COMMANDS: readonly ["lock", "unlock", "deleteWebhook", "deleteScene", "factoryReset"];
|
|
8
|
+
export type DestructiveCommand = (typeof DESTRUCTIVE_COMMANDS)[number];
|
|
9
|
+
/**
|
|
10
|
+
* Parse the verb out of a rule action command string. The expected form
|
|
11
|
+
* mirrors what the engine will eventually build: `devices command <id> <verb> [args...]`.
|
|
12
|
+
* We also accept scene shorthands (`scenes run <id>`, `webhooks delete <id>`).
|
|
13
|
+
*
|
|
14
|
+
* Returns null for anything we cannot confidently attribute to a known verb
|
|
15
|
+
* slot — the validator treats null as "probably fine, let the engine's own
|
|
16
|
+
* guard handle it if it's not."
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractVerb(cmd: string): string | null;
|
|
19
|
+
export declare function isDestructiveCommand(cmd: string): boolean;
|
|
20
|
+
export declare function destructiveVerbOf(cmd: string): DestructiveCommand | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive command parsing — single source of truth shared between the
|
|
3
|
+
* policy validator post-hook (rejects destructive commands inside
|
|
4
|
+
* `automation.rules[].then[].command`) and the runtime executor (second-
|
|
5
|
+
* line guard that refuses to shell out even if validation was bypassed).
|
|
6
|
+
*/
|
|
7
|
+
export const DESTRUCTIVE_COMMANDS = [
|
|
8
|
+
'lock',
|
|
9
|
+
'unlock',
|
|
10
|
+
'deleteWebhook',
|
|
11
|
+
'deleteScene',
|
|
12
|
+
'factoryReset',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Parse the verb out of a rule action command string. The expected form
|
|
16
|
+
* mirrors what the engine will eventually build: `devices command <id> <verb> [args...]`.
|
|
17
|
+
* We also accept scene shorthands (`scenes run <id>`, `webhooks delete <id>`).
|
|
18
|
+
*
|
|
19
|
+
* Returns null for anything we cannot confidently attribute to a known verb
|
|
20
|
+
* slot — the validator treats null as "probably fine, let the engine's own
|
|
21
|
+
* guard handle it if it's not."
|
|
22
|
+
*/
|
|
23
|
+
export function extractVerb(cmd) {
|
|
24
|
+
const trimmed = cmd.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
return null;
|
|
27
|
+
const tokens = trimmed.split(/\s+/);
|
|
28
|
+
// `devices command <id> <verb> [args]`
|
|
29
|
+
if (tokens[0] === 'devices' && tokens[1] === 'command' && tokens.length >= 4) {
|
|
30
|
+
return tokens[3];
|
|
31
|
+
}
|
|
32
|
+
// `webhooks delete <id>` → verb is "deleteWebhook"
|
|
33
|
+
if (tokens[0] === 'webhooks' && tokens[1] === 'delete')
|
|
34
|
+
return 'deleteWebhook';
|
|
35
|
+
// `scenes delete <id>` → verb is "deleteScene"
|
|
36
|
+
if (tokens[0] === 'scenes' && tokens[1] === 'delete')
|
|
37
|
+
return 'deleteScene';
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function isDestructiveCommand(cmd) {
|
|
41
|
+
const verb = extractVerb(cmd);
|
|
42
|
+
if (!verb)
|
|
43
|
+
return false;
|
|
44
|
+
return DESTRUCTIVE_COMMANDS.includes(verb);
|
|
45
|
+
}
|
|
46
|
+
export function destructiveVerbOf(cmd) {
|
|
47
|
+
const verb = extractVerb(cmd);
|
|
48
|
+
if (verb && DESTRUCTIVE_COMMANDS.includes(verb)) {
|
|
49
|
+
return verb;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=destructive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destructive.js","sourceRoot":"","sources":["../../src/rules/destructive.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,MAAM;IACN,QAAQ;IACR,eAAe;IACf,aAAa;IACb,cAAc;CACN,CAAC;AAIX;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAEpC,uCAAuC;IACvC,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QAC7E,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;IACD,mDAAmD;IACnD,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,eAAe,CAAC;IAC/E,+CAA+C;IAC/C,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC;IAC3E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAQ,oBAA0C,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,IAAI,IAAK,oBAA0C,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO,IAA0B,CAAC;IACpC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules engine runtime — orchestrates trigger subscription, matcher
|
|
3
|
+
* pipeline, throttle gate, and action executor.
|
|
4
|
+
*
|
|
5
|
+
* v0.2 PoC scope:
|
|
6
|
+
* - Loads an `automation` block from a policy file.
|
|
7
|
+
* - Subscribes to a single MQTT client; routes every shadow message
|
|
8
|
+
* through `matchesMqttTrigger` → `evaluateConditions` → throttle →
|
|
9
|
+
* `executeRuleAction`.
|
|
10
|
+
* - Cron + webhook triggers are **recognised but not wired** — they
|
|
11
|
+
* surface in the static lint as `unsupported` so users know the
|
|
12
|
+
* feature is pending (E1/E2 fill it in without a schema change).
|
|
13
|
+
* - Exposes `start()`, `stop()`, `getStats()` for the rules run
|
|
14
|
+
* subcommand.
|
|
15
|
+
*
|
|
16
|
+
* Not responsible for: loading the policy file, validating it, talking
|
|
17
|
+
* to the SwitchBot REST API (that's `executeCommand`), or writing
|
|
18
|
+
* audit lines (that's each module's local responsibility).
|
|
19
|
+
*/
|
|
20
|
+
import type { AxiosInstance } from 'axios';
|
|
21
|
+
import type { SwitchBotMqttClient } from '../mqtt/client.js';
|
|
22
|
+
import type { MqttCredential } from '../mqtt/credential.js';
|
|
23
|
+
import { type DeviceStatusFetcher } from './matcher.js';
|
|
24
|
+
import { type AutomationBlock, type EngineEvent, type Rule } from './types.js';
|
|
25
|
+
export interface LintIssue {
|
|
26
|
+
rule: string;
|
|
27
|
+
severity: 'error' | 'warning';
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
export interface LintResult {
|
|
32
|
+
rules: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
status: 'ok' | 'error' | 'unsupported' | 'disabled';
|
|
36
|
+
issues: LintIssue[];
|
|
37
|
+
}>;
|
|
38
|
+
valid: boolean;
|
|
39
|
+
unsupportedCount: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function lintRules(automation: AutomationBlock | null | undefined): LintResult;
|
|
42
|
+
export interface RulesEngineOptions {
|
|
43
|
+
automation: AutomationBlock | null | undefined;
|
|
44
|
+
aliases: Record<string, string>;
|
|
45
|
+
/** Pre-connected MQTT client — owned by the caller. */
|
|
46
|
+
mqttClient: SwitchBotMqttClient;
|
|
47
|
+
/** Credential exposed so we know the default shadow topic to subscribe to. */
|
|
48
|
+
mqttCredential: MqttCredential;
|
|
49
|
+
/** Optional HTTP client for executeCommand — omit in tests. */
|
|
50
|
+
httpClient?: AxiosInstance;
|
|
51
|
+
/** When true, treat every rule as dry_run regardless of policy. */
|
|
52
|
+
globalDryRun?: boolean;
|
|
53
|
+
/** Max firings before the engine self-stops — test / demo only. */
|
|
54
|
+
maxFirings?: number;
|
|
55
|
+
/** Suppress live API calls. Used by tests that don't want to mock axios. */
|
|
56
|
+
skipApiCall?: boolean;
|
|
57
|
+
/** Side channel for unit tests — drop every processed event here. */
|
|
58
|
+
onFire?: (entry: EngineFireEntry) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Webhook bearer token. Required when any rule uses a webhook
|
|
61
|
+
* trigger; the listener will refuse to start otherwise.
|
|
62
|
+
*/
|
|
63
|
+
webhookToken?: string;
|
|
64
|
+
/** Webhook listener port (default 18790). Set 0 to auto-allocate. */
|
|
65
|
+
webhookPort?: number;
|
|
66
|
+
/** Webhook listener host (default 127.0.0.1). */
|
|
67
|
+
webhookHost?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Override how device_state conditions fetch live status. Primarily a
|
|
70
|
+
* test seam — production callers should leave it unset so the engine
|
|
71
|
+
* goes through the normal `fetchDeviceStatus` path with the shared
|
|
72
|
+
* axios client.
|
|
73
|
+
*/
|
|
74
|
+
statusFetcher?: DeviceStatusFetcher;
|
|
75
|
+
}
|
|
76
|
+
export interface EngineFireEntry {
|
|
77
|
+
ruleName: string;
|
|
78
|
+
fireId: string;
|
|
79
|
+
/** Final disposition of the fire. */
|
|
80
|
+
status: 'fired' | 'dry' | 'throttled' | 'conditions-failed' | 'unsupported' | 'blocked';
|
|
81
|
+
deviceId?: string;
|
|
82
|
+
reason?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface EngineStats {
|
|
85
|
+
started: boolean;
|
|
86
|
+
rulesLoaded: number;
|
|
87
|
+
rulesActive: number;
|
|
88
|
+
eventsProcessed: number;
|
|
89
|
+
fires: number;
|
|
90
|
+
dryFires: number;
|
|
91
|
+
throttled: number;
|
|
92
|
+
conditionsFailed: number;
|
|
93
|
+
}
|
|
94
|
+
export declare class RulesEngine {
|
|
95
|
+
private readonly opts;
|
|
96
|
+
private rules;
|
|
97
|
+
private aliases;
|
|
98
|
+
private readonly throttle;
|
|
99
|
+
/** hysteresis / requires_stable_for: tracks when each (rule::device) trigger was first seen. */
|
|
100
|
+
private hysteresisFirstSeen;
|
|
101
|
+
private unsubscribeMessage;
|
|
102
|
+
private unsubscribeState;
|
|
103
|
+
private cronScheduler;
|
|
104
|
+
private webhookListener;
|
|
105
|
+
private started;
|
|
106
|
+
private stopped;
|
|
107
|
+
/**
|
|
108
|
+
* Sequential dispatch queue. Two MQTT messages arriving in the same
|
|
109
|
+
* tick would otherwise race inside the throttle check — each sees an
|
|
110
|
+
* empty lastFireAt map because neither has recorded yet. Serialising
|
|
111
|
+
* keeps the semantics of `max_per` honest.
|
|
112
|
+
*/
|
|
113
|
+
private pendingChain;
|
|
114
|
+
private stats;
|
|
115
|
+
constructor(opts: RulesEngineOptions);
|
|
116
|
+
getStats(): EngineStats;
|
|
117
|
+
getRules(): readonly Rule[];
|
|
118
|
+
/**
|
|
119
|
+
* Subscribes to MQTT and begins the pipeline. Throws if the policy
|
|
120
|
+
* block is missing `enabled: true` or if lint finds errors (e.g.
|
|
121
|
+
* destructive command in a rule action).
|
|
122
|
+
*/
|
|
123
|
+
start(): Promise<void>;
|
|
124
|
+
stop(): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Hot-reload the running engine with a fresh automation block and
|
|
127
|
+
* alias map — typically triggered by SIGHUP or by the `rules reload`
|
|
128
|
+
* subcommand writing the reload sentinel file.
|
|
129
|
+
*
|
|
130
|
+
* Semantics:
|
|
131
|
+
* - Rejects (and keeps the old ruleset) when the new automation is
|
|
132
|
+
* disabled or fails lint. The engine never silently degrades.
|
|
133
|
+
* - Diffs cron registrations by `rule.name` + `schedule`: unchanged
|
|
134
|
+
* entries keep their armed timer, changed/removed entries are
|
|
135
|
+
* unregistered, new entries are registered and armed.
|
|
136
|
+
* - Hands the fresh webhook rule list to the live listener (keeps
|
|
137
|
+
* the bound port / open connections). If the reload removes every
|
|
138
|
+
* webhook rule the listener is torn down; if it adds the first
|
|
139
|
+
* webhook rule we refuse — spinning up a new listener mid-run
|
|
140
|
+
* would silently change the security surface.
|
|
141
|
+
* - `ThrottleGate` state is retained for surviving rule names and
|
|
142
|
+
* dropped for removed ones. A rule that was throttled before the
|
|
143
|
+
* reload stays throttled after it (same name = same window), but
|
|
144
|
+
* a renamed rule resets.
|
|
145
|
+
*/
|
|
146
|
+
reload(nextAutomation: AutomationBlock | null | undefined, nextAliases?: Record<string, string>): Promise<{
|
|
147
|
+
changed: boolean;
|
|
148
|
+
errors: string[];
|
|
149
|
+
warnings: string[];
|
|
150
|
+
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Expose the MQTT pipeline for direct invocation from tests — feeds a
|
|
153
|
+
* synthetic payload through the same matcher/throttle/action chain.
|
|
154
|
+
*/
|
|
155
|
+
ingestMqttForTest(payload: unknown): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Dispatch a pre-built EngineEvent through all matching MQTT rules.
|
|
158
|
+
* Used by tests that need full control over the event timestamp (e.g.
|
|
159
|
+
* hysteresis tests that advance time manually).
|
|
160
|
+
*/
|
|
161
|
+
ingestEventForTest(event: EngineEvent): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Fire a cron rule directly without needing the scheduler/timers.
|
|
164
|
+
* Used by tests that want to exercise the dispatch pipeline without
|
|
165
|
+
* depending on fake timers or croner's internals.
|
|
166
|
+
*/
|
|
167
|
+
ingestCronForTest(rule: Rule, when?: Date): Promise<void>;
|
|
168
|
+
/**
|
|
169
|
+
* Fire a webhook rule directly without standing up the HTTP listener.
|
|
170
|
+
*/
|
|
171
|
+
ingestWebhookForTest(rule: Rule, body?: string, when?: Date): Promise<void>;
|
|
172
|
+
/** Returns the bound webhook port when the listener is active. */
|
|
173
|
+
getWebhookPort(): number | null;
|
|
174
|
+
/** Read-only peek at cron schedule state — for `rules list` extras. */
|
|
175
|
+
getCronSchedule(ruleName: string): {
|
|
176
|
+
schedule: string;
|
|
177
|
+
nextAt: Date | null;
|
|
178
|
+
} | null;
|
|
179
|
+
/** Test helper — resolves after all queued dispatches complete. */
|
|
180
|
+
drainForTest(): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Append a task to the dispatch queue; callers get back a promise that
|
|
183
|
+
* resolves when their task finishes (errors are swallowed — we never
|
|
184
|
+
* want the queue itself to die because one rule threw). Returning a
|
|
185
|
+
* promise lets awaited callsites (ingestMqttForTest) observe completion.
|
|
186
|
+
*/
|
|
187
|
+
private enqueue;
|
|
188
|
+
private onMqttMessage;
|
|
189
|
+
private onCronFire;
|
|
190
|
+
private onWebhookFire;
|
|
191
|
+
private firesTotal;
|
|
192
|
+
private dispatchRule;
|
|
193
|
+
}
|