@switchbot/openapi-cli 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +56945 -169
- package/dist/policy/schema/v0.2.json +1 -1
- package/package.json +3 -2
- package/dist/api/client.js +0 -235
- package/dist/auth.js +0 -20
- package/dist/commands/agent-bootstrap.js +0 -182
- package/dist/commands/auth.js +0 -354
- package/dist/commands/batch.js +0 -413
- package/dist/commands/cache.js +0 -126
- package/dist/commands/capabilities.js +0 -385
- package/dist/commands/catalog.js +0 -359
- package/dist/commands/completion.js +0 -385
- package/dist/commands/config.js +0 -376
- package/dist/commands/daemon.js +0 -410
- package/dist/commands/device-meta.js +0 -159
- package/dist/commands/devices.js +0 -948
- package/dist/commands/doctor.js +0 -1015
- package/dist/commands/events.js +0 -563
- package/dist/commands/expand.js +0 -130
- package/dist/commands/explain.js +0 -139
- package/dist/commands/health.js +0 -113
- package/dist/commands/history.js +0 -320
- package/dist/commands/identity.js +0 -59
- package/dist/commands/install.js +0 -246
- package/dist/commands/mcp.js +0 -2017
- package/dist/commands/plan.js +0 -653
- package/dist/commands/policy.js +0 -586
- package/dist/commands/quota.js +0 -78
- package/dist/commands/rules.js +0 -875
- package/dist/commands/scenes.js +0 -264
- package/dist/commands/schema.js +0 -177
- package/dist/commands/status-sync.js +0 -131
- package/dist/commands/uninstall.js +0 -237
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/watch.js +0 -194
- package/dist/commands/webhook.js +0 -182
- package/dist/config.js +0 -258
- package/dist/credentials/backends/file.js +0 -101
- package/dist/credentials/backends/linux.js +0 -129
- package/dist/credentials/backends/macos.js +0 -129
- package/dist/credentials/backends/windows.js +0 -215
- package/dist/credentials/keychain.js +0 -88
- package/dist/credentials/prime.js +0 -52
- package/dist/devices/cache.js +0 -293
- package/dist/devices/catalog.js +0 -767
- package/dist/devices/device-meta.js +0 -56
- package/dist/devices/history-agg.js +0 -138
- package/dist/devices/history-query.js +0 -181
- package/dist/devices/param-validator.js +0 -433
- package/dist/devices/resources.js +0 -270
- package/dist/install/default-steps.js +0 -257
- package/dist/install/preflight.js +0 -212
- package/dist/install/steps.js +0 -67
- package/dist/lib/command-keywords.js +0 -17
- package/dist/lib/daemon-state.js +0 -46
- package/dist/lib/destructive-mode.js +0 -12
- package/dist/lib/devices.js +0 -382
- package/dist/lib/idempotency.js +0 -106
- package/dist/lib/plan-store.js +0 -68
- package/dist/lib/request-context.js +0 -12
- package/dist/lib/scenes.js +0 -10
- package/dist/logger.js +0 -16
- package/dist/mcp/device-history.js +0 -145
- package/dist/mcp/events-subscription.js +0 -213
- package/dist/mqtt/client.js +0 -180
- package/dist/mqtt/credential.js +0 -30
- package/dist/policy/add-rule.js +0 -124
- package/dist/policy/diff.js +0 -91
- package/dist/policy/format.js +0 -57
- package/dist/policy/load.js +0 -61
- package/dist/policy/migrate.js +0 -67
- package/dist/policy/schema.js +0 -18
- package/dist/policy/validate.js +0 -262
- package/dist/rules/action.js +0 -216
- package/dist/rules/audit-query.js +0 -89
- package/dist/rules/conflict-analyzer.js +0 -214
- package/dist/rules/cron-scheduler.js +0 -186
- package/dist/rules/destructive.js +0 -52
- package/dist/rules/engine.js +0 -757
- package/dist/rules/matcher.js +0 -230
- package/dist/rules/pid-file.js +0 -95
- package/dist/rules/quiet-hours.js +0 -45
- package/dist/rules/suggest.js +0 -95
- package/dist/rules/throttle.js +0 -116
- package/dist/rules/types.js +0 -34
- package/dist/rules/webhook-listener.js +0 -223
- package/dist/rules/webhook-token.js +0 -90
- package/dist/schema/field-aliases.js +0 -131
- package/dist/sinks/dispatcher.js +0 -12
- package/dist/sinks/file.js +0 -19
- package/dist/sinks/format.js +0 -56
- package/dist/sinks/homeassistant.js +0 -44
- package/dist/sinks/openclaw.js +0 -33
- package/dist/sinks/stdout.js +0 -5
- package/dist/sinks/telegram.js +0 -28
- package/dist/sinks/types.js +0 -1
- package/dist/sinks/webhook.js +0 -22
- package/dist/status-sync/manager.js +0 -268
- package/dist/utils/arg-parsers.js +0 -66
- package/dist/utils/audit.js +0 -121
- package/dist/utils/filter.js +0 -189
- package/dist/utils/flags.js +0 -186
- package/dist/utils/format.js +0 -117
- package/dist/utils/health.js +0 -101
- package/dist/utils/help-json.js +0 -54
- package/dist/utils/name-resolver.js +0 -137
- package/dist/utils/output.js +0 -404
- package/dist/utils/quota.js +0 -227
- package/dist/utils/redact.js +0 -68
- package/dist/utils/retry.js +0 -140
- package/dist/utils/string.js +0 -22
- package/dist/version.js +0 -4
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { tryLoadConfig } from '../config.js';
|
|
6
|
-
import { getActiveProfile } from '../lib/request-context.js';
|
|
7
|
-
import { UsageError } from '../utils/output.js';
|
|
8
|
-
import { getConfigPath } from '../utils/flags.js';
|
|
9
|
-
const DEFAULT_OPENCLAW_URL = 'http://localhost:18789';
|
|
10
|
-
function resolveStatusSyncRuntime(options) {
|
|
11
|
-
if (!tryLoadConfig()) {
|
|
12
|
-
throw new UsageError('No credentials found. Run \'switchbot config set-token\' or set SWITCHBOT_TOKEN and SWITCHBOT_SECRET.');
|
|
13
|
-
}
|
|
14
|
-
const openclawToken = options.openclawToken ?? process.env.OPENCLAW_TOKEN;
|
|
15
|
-
if (!openclawToken) {
|
|
16
|
-
throw new UsageError('--openclaw-token is required or set OPENCLAW_TOKEN in the environment.');
|
|
17
|
-
}
|
|
18
|
-
const openclawModel = options.openclawModel ?? process.env.OPENCLAW_MODEL;
|
|
19
|
-
if (!openclawModel) {
|
|
20
|
-
throw new UsageError('--openclaw-model is required or set OPENCLAW_MODEL in the environment.');
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
openclawUrl: options.openclawUrl ?? process.env.OPENCLAW_URL ?? DEFAULT_OPENCLAW_URL,
|
|
24
|
-
openclawToken,
|
|
25
|
-
openclawModel,
|
|
26
|
-
...(options.topic ? { topic: options.topic } : {}),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
export function resolveStatusSyncPaths(explicitStateDir) {
|
|
30
|
-
const stateDir = path.resolve(explicitStateDir
|
|
31
|
-
?? process.env.SWITCHBOT_STATUS_SYNC_HOME
|
|
32
|
-
?? path.join(os.homedir(), '.switchbot', 'status-sync'));
|
|
33
|
-
return {
|
|
34
|
-
stateDir,
|
|
35
|
-
stateFile: path.join(stateDir, 'state.json'),
|
|
36
|
-
stdoutLog: path.join(stateDir, 'stdout.log'),
|
|
37
|
-
stderrLog: path.join(stateDir, 'stderr.log'),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
export function buildStatusSyncChildArgs(options) {
|
|
41
|
-
const scriptPath = process.argv[1];
|
|
42
|
-
if (!scriptPath) {
|
|
43
|
-
throw new Error('Cannot determine the current CLI entrypoint path.');
|
|
44
|
-
}
|
|
45
|
-
const args = [path.resolve(scriptPath)];
|
|
46
|
-
const configPath = getConfigPath();
|
|
47
|
-
const profile = getActiveProfile();
|
|
48
|
-
if (configPath) {
|
|
49
|
-
args.push('--config', path.resolve(configPath));
|
|
50
|
-
}
|
|
51
|
-
else if (profile) {
|
|
52
|
-
args.push('--profile', profile);
|
|
53
|
-
}
|
|
54
|
-
args.push('events', 'mqtt-tail', '--sink', 'openclaw', '--openclaw-url', options.openclawUrl, '--openclaw-model', options.openclawModel);
|
|
55
|
-
if (options.topic) {
|
|
56
|
-
args.push('--topic', options.topic);
|
|
57
|
-
}
|
|
58
|
-
return args;
|
|
59
|
-
}
|
|
60
|
-
function safeUnlink(filePath) {
|
|
61
|
-
try {
|
|
62
|
-
fs.unlinkSync(filePath);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// best-effort cleanup
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function isProcessRunning(pid) {
|
|
69
|
-
try {
|
|
70
|
-
process.kill(pid, 0);
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
const code = err.code;
|
|
75
|
-
if (code === 'EPERM')
|
|
76
|
-
return true;
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function readStateFile(paths) {
|
|
81
|
-
if (!fs.existsSync(paths.stateFile))
|
|
82
|
-
return null;
|
|
83
|
-
try {
|
|
84
|
-
const raw = JSON.parse(fs.readFileSync(paths.stateFile, 'utf-8'));
|
|
85
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
86
|
-
safeUnlink(paths.stateFile);
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
const parsed = raw;
|
|
90
|
-
if (typeof parsed.pid !== 'number' ||
|
|
91
|
-
!Number.isInteger(parsed.pid) ||
|
|
92
|
-
parsed.pid < 1 ||
|
|
93
|
-
typeof parsed.startedAt !== 'string' ||
|
|
94
|
-
!Array.isArray(parsed.command) ||
|
|
95
|
-
typeof parsed.stdoutLog !== 'string' ||
|
|
96
|
-
typeof parsed.stderrLog !== 'string') {
|
|
97
|
-
safeUnlink(paths.stateFile);
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
pid: parsed.pid,
|
|
102
|
-
startedAt: parsed.startedAt,
|
|
103
|
-
command: parsed.command.map(String),
|
|
104
|
-
openclawUrl: typeof parsed.openclawUrl === 'string' ? parsed.openclawUrl : DEFAULT_OPENCLAW_URL,
|
|
105
|
-
openclawModel: typeof parsed.openclawModel === 'string' ? parsed.openclawModel : '',
|
|
106
|
-
topic: typeof parsed.topic === 'string' ? parsed.topic : null,
|
|
107
|
-
configPath: typeof parsed.configPath === 'string' ? parsed.configPath : null,
|
|
108
|
-
profile: typeof parsed.profile === 'string' ? parsed.profile : null,
|
|
109
|
-
stdoutLog: parsed.stdoutLog,
|
|
110
|
-
stderrLog: parsed.stderrLog,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
safeUnlink(paths.stateFile);
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function toStatus(paths, state, running) {
|
|
119
|
-
return {
|
|
120
|
-
running,
|
|
121
|
-
pid: running && state ? state.pid : null,
|
|
122
|
-
startedAt: running && state ? state.startedAt : null,
|
|
123
|
-
stateDir: paths.stateDir,
|
|
124
|
-
stateFile: paths.stateFile,
|
|
125
|
-
stdoutLog: state?.stdoutLog ?? paths.stdoutLog,
|
|
126
|
-
stderrLog: state?.stderrLog ?? paths.stderrLog,
|
|
127
|
-
command: running && state ? state.command : null,
|
|
128
|
-
openclawUrl: running && state ? state.openclawUrl : null,
|
|
129
|
-
openclawModel: running && state ? state.openclawModel : null,
|
|
130
|
-
topic: running && state ? state.topic : null,
|
|
131
|
-
configPath: running && state ? state.configPath : null,
|
|
132
|
-
profile: running && state ? state.profile : null,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function killProcessTree(pid) {
|
|
136
|
-
if (process.platform === 'win32') {
|
|
137
|
-
const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
138
|
-
if (result.error)
|
|
139
|
-
throw result.error;
|
|
140
|
-
if (result.status !== 0 && isProcessRunning(pid)) {
|
|
141
|
-
throw new Error(`Failed to stop status-sync process tree (PID ${pid}).`);
|
|
142
|
-
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
process.kill(-pid, 'SIGTERM');
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
const code = err.code;
|
|
150
|
-
if (code === 'ESRCH') {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
process.kill(pid, 'SIGTERM');
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
export function getStatusSyncStatus(options = {}) {
|
|
157
|
-
const paths = resolveStatusSyncPaths(options.stateDir);
|
|
158
|
-
const state = readStateFile(paths);
|
|
159
|
-
if (!state) {
|
|
160
|
-
return toStatus(paths, null, false);
|
|
161
|
-
}
|
|
162
|
-
if (!isProcessRunning(state.pid)) {
|
|
163
|
-
safeUnlink(paths.stateFile);
|
|
164
|
-
return toStatus(paths, null, false);
|
|
165
|
-
}
|
|
166
|
-
return toStatus(paths, state, true);
|
|
167
|
-
}
|
|
168
|
-
export function stopStatusSync(options = {}) {
|
|
169
|
-
const paths = resolveStatusSyncPaths(options.stateDir);
|
|
170
|
-
const state = readStateFile(paths);
|
|
171
|
-
if (!state) {
|
|
172
|
-
return {
|
|
173
|
-
stopped: false,
|
|
174
|
-
stale: false,
|
|
175
|
-
pid: null,
|
|
176
|
-
status: toStatus(paths, null, false),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (!isProcessRunning(state.pid)) {
|
|
180
|
-
safeUnlink(paths.stateFile);
|
|
181
|
-
return {
|
|
182
|
-
stopped: false,
|
|
183
|
-
stale: true,
|
|
184
|
-
pid: state.pid,
|
|
185
|
-
status: toStatus(paths, null, false),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
killProcessTree(state.pid);
|
|
189
|
-
if (isProcessRunning(state.pid)) {
|
|
190
|
-
throw new Error(`Failed to stop status-sync process (PID ${state.pid}); process is still running.`);
|
|
191
|
-
}
|
|
192
|
-
safeUnlink(paths.stateFile);
|
|
193
|
-
return {
|
|
194
|
-
stopped: true,
|
|
195
|
-
stale: false,
|
|
196
|
-
pid: state.pid,
|
|
197
|
-
status: toStatus(paths, null, false),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
export function startStatusSync(options = {}) {
|
|
201
|
-
const runtime = resolveStatusSyncRuntime(options);
|
|
202
|
-
const paths = resolveStatusSyncPaths(options.stateDir);
|
|
203
|
-
const existing = getStatusSyncStatus({ stateDir: paths.stateDir });
|
|
204
|
-
if (existing.running) {
|
|
205
|
-
if (!options.force) {
|
|
206
|
-
throw new UsageError(`status-sync is already running (PID ${existing.pid}). Run 'switchbot status-sync stop' first or re-run with --force.`);
|
|
207
|
-
}
|
|
208
|
-
stopStatusSync({ stateDir: paths.stateDir });
|
|
209
|
-
}
|
|
210
|
-
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
211
|
-
const configPath = getConfigPath();
|
|
212
|
-
const command = buildStatusSyncChildArgs(runtime);
|
|
213
|
-
let stdoutFd = null;
|
|
214
|
-
let stderrFd = null;
|
|
215
|
-
try {
|
|
216
|
-
stdoutFd = fs.openSync(paths.stdoutLog, 'a');
|
|
217
|
-
stderrFd = fs.openSync(paths.stderrLog, 'a');
|
|
218
|
-
const child = spawn(process.execPath, command, {
|
|
219
|
-
detached: true,
|
|
220
|
-
stdio: ['ignore', stdoutFd, stderrFd],
|
|
221
|
-
windowsHide: true,
|
|
222
|
-
env: { ...process.env, OPENCLAW_TOKEN: runtime.openclawToken },
|
|
223
|
-
});
|
|
224
|
-
if (!child.pid) {
|
|
225
|
-
throw new Error('Failed to start status-sync child process.');
|
|
226
|
-
}
|
|
227
|
-
child.unref();
|
|
228
|
-
const state = {
|
|
229
|
-
pid: child.pid,
|
|
230
|
-
startedAt: new Date().toISOString(),
|
|
231
|
-
command: [process.execPath, ...command],
|
|
232
|
-
openclawUrl: runtime.openclawUrl,
|
|
233
|
-
openclawModel: runtime.openclawModel,
|
|
234
|
-
topic: runtime.topic ?? null,
|
|
235
|
-
configPath: configPath ? path.resolve(configPath) : null,
|
|
236
|
-
profile: configPath ? null : (getActiveProfile() ?? null),
|
|
237
|
-
stdoutLog: paths.stdoutLog,
|
|
238
|
-
stderrLog: paths.stderrLog,
|
|
239
|
-
};
|
|
240
|
-
fs.writeFileSync(paths.stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
241
|
-
return toStatus(paths, state, true);
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
if (stdoutFd !== null)
|
|
245
|
-
fs.closeSync(stdoutFd);
|
|
246
|
-
if (stderrFd !== null)
|
|
247
|
-
fs.closeSync(stderrFd);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
export async function runStatusSyncForeground(options = {}) {
|
|
251
|
-
const runtime = resolveStatusSyncRuntime(options);
|
|
252
|
-
const command = buildStatusSyncChildArgs(runtime);
|
|
253
|
-
return await new Promise((resolve, reject) => {
|
|
254
|
-
const child = spawn(process.execPath, command, {
|
|
255
|
-
stdio: 'inherit',
|
|
256
|
-
windowsHide: true,
|
|
257
|
-
env: { ...process.env, OPENCLAW_TOKEN: runtime.openclawToken },
|
|
258
|
-
});
|
|
259
|
-
child.once('error', reject);
|
|
260
|
-
child.once('exit', (code, signal) => {
|
|
261
|
-
if (signal) {
|
|
262
|
-
resolve(1);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
resolve(code ?? 0);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { InvalidArgumentError } from 'commander';
|
|
2
|
-
import { parseDurationToMs } from './flags.js';
|
|
3
|
-
/**
|
|
4
|
-
* Commander argParser callbacks that fail fast when a required-value flag
|
|
5
|
-
* swallows the next token (another flag, a subcommand name, etc.) — the
|
|
6
|
-
* default Commander behavior is to take the next argv token verbatim.
|
|
7
|
-
*
|
|
8
|
-
* Use `--flag=<val>` form to pass values that legitimately start with `--`.
|
|
9
|
-
*/
|
|
10
|
-
export function intArg(flagName, opts) {
|
|
11
|
-
return (value) => {
|
|
12
|
-
// Flag-like tokens (`--something`, `-x`) are rejected up-front.
|
|
13
|
-
// Pure negative integers (`-1`, `-42`) fall through to min/max so the
|
|
14
|
-
// error classifies as a range error rather than "requires a numeric value".
|
|
15
|
-
if (value.startsWith('-') && !/^-\d+$/.test(value)) {
|
|
16
|
-
throw new InvalidArgumentError(`${flagName} requires a numeric value, got "${value}". ` +
|
|
17
|
-
`Did you forget a value? Use ${flagName}=<n> if the value really starts with "-".`);
|
|
18
|
-
}
|
|
19
|
-
const n = Number(value);
|
|
20
|
-
if (!Number.isInteger(n)) {
|
|
21
|
-
throw new InvalidArgumentError(`${flagName} must be an integer (got "${value}")`);
|
|
22
|
-
}
|
|
23
|
-
if (opts?.min !== undefined && n < opts.min) {
|
|
24
|
-
throw new InvalidArgumentError(`${flagName} must be >= ${opts.min} (got "${value}")`);
|
|
25
|
-
}
|
|
26
|
-
if (opts?.max !== undefined && n > opts.max) {
|
|
27
|
-
throw new InvalidArgumentError(`${flagName} must be <= ${opts.max} (got "${value}")`);
|
|
28
|
-
}
|
|
29
|
-
return String(n);
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
export function durationArg(flagName) {
|
|
33
|
-
return (value) => {
|
|
34
|
-
if (value.startsWith('-')) {
|
|
35
|
-
throw new InvalidArgumentError(`${flagName} requires a duration value, got "${value}". ` +
|
|
36
|
-
`Use ${flagName}=<dur> if the value really starts with "-".`);
|
|
37
|
-
}
|
|
38
|
-
const ms = parseDurationToMs(value);
|
|
39
|
-
if (ms === null) {
|
|
40
|
-
throw new InvalidArgumentError(`${flagName} must look like "30s", "1m", "500ms", "1h", "7d", "2w" ` +
|
|
41
|
-
`(supported units: ms, s, m, h, d, w — got "${value}")`);
|
|
42
|
-
}
|
|
43
|
-
return value;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export function stringArg(flagName, opts) {
|
|
47
|
-
return (value) => {
|
|
48
|
-
if (value.startsWith('--')) {
|
|
49
|
-
throw new InvalidArgumentError(`${flagName} requires a value. "${value}" looks like another option — ` +
|
|
50
|
-
`did you forget the value? Use ${flagName}=<val> if your value really starts with "--".`);
|
|
51
|
-
}
|
|
52
|
-
if (opts?.disallow?.includes(value)) {
|
|
53
|
-
throw new InvalidArgumentError(`${flagName} requires a value but got "${value}", which is a subcommand name. ` +
|
|
54
|
-
`Did you forget the value? Use ${flagName}=<val> or put ${flagName} after the subcommand.`);
|
|
55
|
-
}
|
|
56
|
-
return value;
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
export function enumArg(flagName, allowed) {
|
|
60
|
-
return (value) => {
|
|
61
|
-
if (!allowed.includes(value)) {
|
|
62
|
-
throw new InvalidArgumentError(`${flagName} must be one of: ${allowed.join(', ')} (got "${value}")`);
|
|
63
|
-
}
|
|
64
|
-
return value;
|
|
65
|
-
};
|
|
66
|
-
}
|
package/dist/utils/audit.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { getAuditLog } from './flags.js';
|
|
5
|
-
export const DEFAULT_AUDIT_PATH = path.join(os.homedir(), '.switchbot', 'audit.log');
|
|
6
|
-
/**
|
|
7
|
-
* Bump when breaking changes to the audit line shape land.
|
|
8
|
-
*
|
|
9
|
-
* History:
|
|
10
|
-
* 1 — initial command audit (kind: 'command' only).
|
|
11
|
-
* 2 — adds rule-engine kinds ('rule-fire', 'rule-fire-dry',
|
|
12
|
-
* 'rule-throttled', 'rule-webhook-rejected') and a sibling `rule`
|
|
13
|
-
* block describing which rule fired and why. Reader stays backwards
|
|
14
|
-
* compatible: v1 lines parse as command entries with `rule`
|
|
15
|
-
* undefined.
|
|
16
|
-
*/
|
|
17
|
-
export const AUDIT_VERSION = 2;
|
|
18
|
-
function resolveAuditPath() {
|
|
19
|
-
const flag = getAuditLog();
|
|
20
|
-
if (flag === null)
|
|
21
|
-
return null;
|
|
22
|
-
return path.resolve(flag);
|
|
23
|
-
}
|
|
24
|
-
export function writeAudit(entry) {
|
|
25
|
-
let file = resolveAuditPath();
|
|
26
|
-
if (!file && entry.planId)
|
|
27
|
-
file = DEFAULT_AUDIT_PATH;
|
|
28
|
-
if (!file)
|
|
29
|
-
return;
|
|
30
|
-
const dir = path.dirname(file);
|
|
31
|
-
try {
|
|
32
|
-
if (!fs.existsSync(dir)) {
|
|
33
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
const stamped = { auditVersion: AUDIT_VERSION, ...entry };
|
|
36
|
-
fs.appendFileSync(file, JSON.stringify(stamped) + '\n');
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// Best-effort — never let audit failures break the actual command.
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export function readAudit(file) {
|
|
43
|
-
if (!fs.existsSync(file))
|
|
44
|
-
return [];
|
|
45
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
46
|
-
const out = [];
|
|
47
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
48
|
-
const trimmed = line.trim();
|
|
49
|
-
if (!trimmed)
|
|
50
|
-
continue;
|
|
51
|
-
try {
|
|
52
|
-
out.push(JSON.parse(trimmed));
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// skip malformed lines
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
export function verifyAudit(file) {
|
|
61
|
-
const report = {
|
|
62
|
-
file,
|
|
63
|
-
totalLines: 0,
|
|
64
|
-
parsedLines: 0,
|
|
65
|
-
skippedBlankLines: 0,
|
|
66
|
-
malformedLines: 0,
|
|
67
|
-
unversionedEntries: 0,
|
|
68
|
-
versionCounts: {},
|
|
69
|
-
problems: [],
|
|
70
|
-
};
|
|
71
|
-
if (!fs.existsSync(file)) {
|
|
72
|
-
report.fileMissing = true;
|
|
73
|
-
return report;
|
|
74
|
-
}
|
|
75
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
76
|
-
const lines = raw.split(/\r?\n/);
|
|
77
|
-
let minT;
|
|
78
|
-
let maxT;
|
|
79
|
-
for (let i = 0; i < lines.length; i++) {
|
|
80
|
-
const trimmed = lines[i].trim();
|
|
81
|
-
if (!trimmed) {
|
|
82
|
-
report.skippedBlankLines++;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
report.totalLines++;
|
|
86
|
-
let entry = null;
|
|
87
|
-
try {
|
|
88
|
-
entry = JSON.parse(trimmed);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
report.malformedLines++;
|
|
92
|
-
report.problems.push({
|
|
93
|
-
line: i + 1,
|
|
94
|
-
reason: 'JSON parse failed',
|
|
95
|
-
preview: trimmed.slice(0, 80),
|
|
96
|
-
});
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
report.parsedLines++;
|
|
100
|
-
const v = entry.auditVersion;
|
|
101
|
-
if (v === undefined) {
|
|
102
|
-
report.unversionedEntries++;
|
|
103
|
-
report.versionCounts['unversioned'] = (report.versionCounts['unversioned'] ?? 0) + 1;
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
const key = String(v);
|
|
107
|
-
report.versionCounts[key] = (report.versionCounts[key] ?? 0) + 1;
|
|
108
|
-
}
|
|
109
|
-
if (entry.t) {
|
|
110
|
-
if (!minT || entry.t < minT)
|
|
111
|
-
minT = entry.t;
|
|
112
|
-
if (!maxT || entry.t > maxT)
|
|
113
|
-
maxT = entry.t;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (minT)
|
|
117
|
-
report.earliest = minT;
|
|
118
|
-
if (maxT)
|
|
119
|
-
report.latest = maxT;
|
|
120
|
-
return report;
|
|
121
|
-
}
|
package/dist/utils/filter.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
export class FilterSyntaxError extends Error {
|
|
2
|
-
constructor(message) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = 'FilterSyntaxError';
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Parse a comma-separated filter expression into discrete clauses.
|
|
9
|
-
*
|
|
10
|
-
* Grammar (per clause, recognition order):
|
|
11
|
-
* 1. key=/pattern/ → regex (case-insensitive); invalid regex throws.
|
|
12
|
-
* 2. key!=value → 'neq' op (negated substring; exact-negated for keys
|
|
13
|
-
* listed in matchClause's `exactKeys` option).
|
|
14
|
-
* 3. key~value → substring (case-insensitive).
|
|
15
|
-
* 4. key=value → 'eq' op (substring; caller decides whether to treat
|
|
16
|
-
* as exact for specific keys via matchClause's
|
|
17
|
-
* `exactKeys` option).
|
|
18
|
-
*
|
|
19
|
-
* `allowedKeys` is command-specific: `devices list` uses
|
|
20
|
-
* {type,name,category,room}; `devices batch` uses {type,family,room,category};
|
|
21
|
-
* `events tail` uses {deviceId,type}.
|
|
22
|
-
*/
|
|
23
|
-
export function parseFilterExpr(expr, allowedKeys, options) {
|
|
24
|
-
if (!expr)
|
|
25
|
-
return [];
|
|
26
|
-
const parts = expr.split(',').map((p) => p.trim()).filter((p) => p.length > 0);
|
|
27
|
-
const clauses = [];
|
|
28
|
-
for (const part of parts) {
|
|
29
|
-
const regexMatch = /^([^=~!]+)=\/(.*)\/$/.exec(part);
|
|
30
|
-
const neqIdx = part.indexOf('!=');
|
|
31
|
-
const tildeIdx = part.indexOf('~');
|
|
32
|
-
const eqIdx = part.indexOf('=');
|
|
33
|
-
let key;
|
|
34
|
-
let op;
|
|
35
|
-
let raw;
|
|
36
|
-
let regex;
|
|
37
|
-
if (regexMatch) {
|
|
38
|
-
key = regexMatch[1].trim();
|
|
39
|
-
op = 'regex';
|
|
40
|
-
raw = regexMatch[2];
|
|
41
|
-
try {
|
|
42
|
-
regex = new RegExp(raw, 'i');
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
throw new FilterSyntaxError(`Invalid regex in --filter "${part}": ${err.message}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
else if (neqIdx !== -1 && (tildeIdx === -1 || neqIdx < tildeIdx)) {
|
|
49
|
-
key = part.slice(0, neqIdx).trim();
|
|
50
|
-
op = 'neq';
|
|
51
|
-
raw = part.slice(neqIdx + 2).trim();
|
|
52
|
-
}
|
|
53
|
-
else if (tildeIdx !== -1 && (eqIdx === -1 || tildeIdx < eqIdx)) {
|
|
54
|
-
key = part.slice(0, tildeIdx).trim();
|
|
55
|
-
op = 'sub';
|
|
56
|
-
raw = part.slice(tildeIdx + 1).trim();
|
|
57
|
-
if (raw.startsWith('=')) {
|
|
58
|
-
throw new FilterSyntaxError(`Invalid filter clause "${part}" — "~=" is no longer supported. Use "${key}~${raw.slice(1)}" instead.`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else if (eqIdx !== -1) {
|
|
62
|
-
key = part.slice(0, eqIdx).trim();
|
|
63
|
-
op = 'eq';
|
|
64
|
-
raw = part.slice(eqIdx + 1).trim();
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
throw new FilterSyntaxError(`Invalid filter clause "${part}" — expected "<key>=<value>", "<key>!=<value>", "<key>~<value>", or "<key>=/<regex>/"`);
|
|
68
|
-
}
|
|
69
|
-
if (!key) {
|
|
70
|
-
throw new FilterSyntaxError(`Empty key in filter clause "${part}"`);
|
|
71
|
-
}
|
|
72
|
-
if (!raw) {
|
|
73
|
-
throw new FilterSyntaxError(`Empty value for filter clause "${part}"`);
|
|
74
|
-
}
|
|
75
|
-
let resolvedKey = key;
|
|
76
|
-
if (options?.resolveKey) {
|
|
77
|
-
try {
|
|
78
|
-
resolvedKey = options.resolveKey(key);
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
if (err instanceof Error) {
|
|
82
|
-
throw new FilterSyntaxError(err.message);
|
|
83
|
-
}
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (!allowedKeys.includes(resolvedKey)) {
|
|
88
|
-
const printableKeys = options?.supportedKeys ?? allowedKeys;
|
|
89
|
-
throw new FilterSyntaxError(`Unknown filter key "${key}" – supported: ${printableKeys.join(', ')}`);
|
|
90
|
-
}
|
|
91
|
-
clauses.push({ key: resolvedKey, op, raw, regex });
|
|
92
|
-
}
|
|
93
|
-
return clauses;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Match a single candidate string against a clause.
|
|
97
|
-
*
|
|
98
|
-
* - `regex` → RegExp.test against the candidate (case-insensitive by construction).
|
|
99
|
-
* - `sub` → case-insensitive substring.
|
|
100
|
-
* - `eq` → case-insensitive substring, except for keys listed in
|
|
101
|
-
* `exactKeys`, which get case-insensitive exact comparison.
|
|
102
|
-
* Default `exactKeys` is `['category']` to preserve the existing
|
|
103
|
-
* list/batch behavior for that key.
|
|
104
|
-
* - `neq` → logical inverse of `eq` (negated substring; exact-negated for
|
|
105
|
-
* `exactKeys`). `undefined` candidates remain non-matching so a
|
|
106
|
-
* `neq` clause does NOT accidentally match missing data.
|
|
107
|
-
*/
|
|
108
|
-
export function matchClause(candidate, clause, options) {
|
|
109
|
-
if (candidate === undefined) {
|
|
110
|
-
// Missing field: `neq` treats absence as "definitely not X"; everything
|
|
111
|
-
// else treats it as "no evidence — don't match".
|
|
112
|
-
return clause.op === 'neq';
|
|
113
|
-
}
|
|
114
|
-
if (clause.op === 'regex') {
|
|
115
|
-
return clause.regex.test(candidate);
|
|
116
|
-
}
|
|
117
|
-
const cLower = candidate.toLowerCase();
|
|
118
|
-
const vLower = clause.raw.toLowerCase();
|
|
119
|
-
if (clause.op === 'sub') {
|
|
120
|
-
return cLower.includes(vLower);
|
|
121
|
-
}
|
|
122
|
-
const exactKeys = options?.exactKeys ?? ['category'];
|
|
123
|
-
const exact = exactKeys.includes(clause.key);
|
|
124
|
-
if (clause.op === 'neq') {
|
|
125
|
-
return exact ? cLower !== vLower : !cLower.includes(vLower);
|
|
126
|
-
}
|
|
127
|
-
if (exact) {
|
|
128
|
-
return cLower === vLower;
|
|
129
|
-
}
|
|
130
|
-
return cLower.includes(vLower);
|
|
131
|
-
}
|
|
132
|
-
const BATCH_KEYS = ['type', 'family', 'room', 'category'];
|
|
133
|
-
/**
|
|
134
|
-
* Back-compat narrow signature: parses with the batch key set. Callers that
|
|
135
|
-
* need a different key set (list, events tail) should call parseFilterExpr
|
|
136
|
-
* directly.
|
|
137
|
-
*/
|
|
138
|
-
export function parseFilter(expr) {
|
|
139
|
-
return parseFilterExpr(expr, BATCH_KEYS);
|
|
140
|
-
}
|
|
141
|
-
/** Normalize a physical / IR device entry to the shape the filter matcher expects. */
|
|
142
|
-
function toFilterable(d, isPhysical, hubLocation) {
|
|
143
|
-
if (isPhysical) {
|
|
144
|
-
const p = d;
|
|
145
|
-
return {
|
|
146
|
-
deviceId: p.deviceId,
|
|
147
|
-
type: p.deviceType ?? '',
|
|
148
|
-
family: p.familyName ?? undefined,
|
|
149
|
-
room: p.roomName ?? undefined,
|
|
150
|
-
category: 'physical',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
const ir = d;
|
|
154
|
-
const inherited = hubLocation?.get(ir.hubDeviceId);
|
|
155
|
-
return {
|
|
156
|
-
deviceId: ir.deviceId,
|
|
157
|
-
type: ir.remoteType ?? '',
|
|
158
|
-
family: inherited?.family,
|
|
159
|
-
room: inherited?.room,
|
|
160
|
-
category: 'ir',
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
function candidateFor(d, key) {
|
|
164
|
-
switch (key) {
|
|
165
|
-
case 'type':
|
|
166
|
-
return d.type;
|
|
167
|
-
case 'family':
|
|
168
|
-
return d.family;
|
|
169
|
-
case 'room':
|
|
170
|
-
return d.room;
|
|
171
|
-
case 'category':
|
|
172
|
-
return d.category;
|
|
173
|
-
default:
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Apply the parsed clauses to a mixed list of physical devices + IR remotes.
|
|
179
|
-
* Returns the filterable entries that satisfy every clause.
|
|
180
|
-
*/
|
|
181
|
-
export function applyFilter(clauses, deviceList, infraredRemoteList, hubLocation) {
|
|
182
|
-
const candidates = [
|
|
183
|
-
...deviceList.map((d) => toFilterable(d, true)),
|
|
184
|
-
...infraredRemoteList.map((d) => toFilterable(d, false, hubLocation)),
|
|
185
|
-
];
|
|
186
|
-
if (clauses.length === 0)
|
|
187
|
-
return candidates;
|
|
188
|
-
return candidates.filter((c) => clauses.every((clause) => matchClause(candidateFor(c, clause.key), clause)));
|
|
189
|
-
}
|