@switchbot/openapi-cli 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +56945 -169
- package/dist/policy/schema/v0.2.json +1 -1
- package/package.json +3 -2
- package/dist/api/client.js +0 -235
- package/dist/auth.js +0 -20
- package/dist/commands/agent-bootstrap.js +0 -182
- package/dist/commands/auth.js +0 -354
- package/dist/commands/batch.js +0 -413
- package/dist/commands/cache.js +0 -126
- package/dist/commands/capabilities.js +0 -385
- package/dist/commands/catalog.js +0 -359
- package/dist/commands/completion.js +0 -385
- package/dist/commands/config.js +0 -376
- package/dist/commands/daemon.js +0 -410
- package/dist/commands/device-meta.js +0 -159
- package/dist/commands/devices.js +0 -948
- package/dist/commands/doctor.js +0 -1015
- package/dist/commands/events.js +0 -563
- package/dist/commands/expand.js +0 -130
- package/dist/commands/explain.js +0 -139
- package/dist/commands/health.js +0 -113
- package/dist/commands/history.js +0 -320
- package/dist/commands/identity.js +0 -59
- package/dist/commands/install.js +0 -246
- package/dist/commands/mcp.js +0 -2017
- package/dist/commands/plan.js +0 -653
- package/dist/commands/policy.js +0 -586
- package/dist/commands/quota.js +0 -78
- package/dist/commands/rules.js +0 -875
- package/dist/commands/scenes.js +0 -264
- package/dist/commands/schema.js +0 -177
- package/dist/commands/status-sync.js +0 -131
- package/dist/commands/uninstall.js +0 -237
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/watch.js +0 -194
- package/dist/commands/webhook.js +0 -182
- package/dist/config.js +0 -258
- package/dist/credentials/backends/file.js +0 -101
- package/dist/credentials/backends/linux.js +0 -129
- package/dist/credentials/backends/macos.js +0 -129
- package/dist/credentials/backends/windows.js +0 -215
- package/dist/credentials/keychain.js +0 -88
- package/dist/credentials/prime.js +0 -52
- package/dist/devices/cache.js +0 -293
- package/dist/devices/catalog.js +0 -767
- package/dist/devices/device-meta.js +0 -56
- package/dist/devices/history-agg.js +0 -138
- package/dist/devices/history-query.js +0 -181
- package/dist/devices/param-validator.js +0 -433
- package/dist/devices/resources.js +0 -270
- package/dist/install/default-steps.js +0 -257
- package/dist/install/preflight.js +0 -212
- package/dist/install/steps.js +0 -67
- package/dist/lib/command-keywords.js +0 -17
- package/dist/lib/daemon-state.js +0 -46
- package/dist/lib/destructive-mode.js +0 -12
- package/dist/lib/devices.js +0 -382
- package/dist/lib/idempotency.js +0 -106
- package/dist/lib/plan-store.js +0 -68
- package/dist/lib/request-context.js +0 -12
- package/dist/lib/scenes.js +0 -10
- package/dist/logger.js +0 -16
- package/dist/mcp/device-history.js +0 -145
- package/dist/mcp/events-subscription.js +0 -213
- package/dist/mqtt/client.js +0 -180
- package/dist/mqtt/credential.js +0 -30
- package/dist/policy/add-rule.js +0 -124
- package/dist/policy/diff.js +0 -91
- package/dist/policy/format.js +0 -57
- package/dist/policy/load.js +0 -61
- package/dist/policy/migrate.js +0 -67
- package/dist/policy/schema.js +0 -18
- package/dist/policy/validate.js +0 -262
- package/dist/rules/action.js +0 -216
- package/dist/rules/audit-query.js +0 -89
- package/dist/rules/conflict-analyzer.js +0 -214
- package/dist/rules/cron-scheduler.js +0 -186
- package/dist/rules/destructive.js +0 -52
- package/dist/rules/engine.js +0 -757
- package/dist/rules/matcher.js +0 -230
- package/dist/rules/pid-file.js +0 -95
- package/dist/rules/quiet-hours.js +0 -45
- package/dist/rules/suggest.js +0 -95
- package/dist/rules/throttle.js +0 -116
- package/dist/rules/types.js +0 -34
- package/dist/rules/webhook-listener.js +0 -223
- package/dist/rules/webhook-token.js +0 -90
- package/dist/schema/field-aliases.js +0 -131
- package/dist/sinks/dispatcher.js +0 -12
- package/dist/sinks/file.js +0 -19
- package/dist/sinks/format.js +0 -56
- package/dist/sinks/homeassistant.js +0 -44
- package/dist/sinks/openclaw.js +0 -33
- package/dist/sinks/stdout.js +0 -5
- package/dist/sinks/telegram.js +0 -28
- package/dist/sinks/types.js +0 -1
- package/dist/sinks/webhook.js +0 -22
- package/dist/status-sync/manager.js +0 -268
- package/dist/utils/arg-parsers.js +0 -66
- package/dist/utils/audit.js +0 -121
- package/dist/utils/filter.js +0 -189
- package/dist/utils/flags.js +0 -186
- package/dist/utils/format.js +0 -117
- package/dist/utils/health.js +0 -101
- package/dist/utils/help-json.js +0 -54
- package/dist/utils/name-resolver.js +0 -137
- package/dist/utils/output.js +0 -404
- package/dist/utils/quota.js +0 -227
- package/dist/utils/redact.js +0 -68
- package/dist/utils/retry.js +0 -140
- package/dist/utils/string.js +0 -22
- package/dist/version.js +0 -4
package/dist/commands/daemon.js
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { isJsonMode, printJson, exitWithError } from '../utils/output.js';
|
|
6
|
-
import { readPidFile, writePidFile, clearPidFile, isPidAlive, getDefaultPidFilePaths, writeReloadSentinel, sighupSupported } from '../rules/pid-file.js';
|
|
7
|
-
import { stringArg } from '../utils/arg-parsers.js';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import { DAEMON_LOG_FILE, DAEMON_PID_FILE, DAEMON_STATE_FILE, HEALTHZ_PID_FILE, readDaemonState, writeDaemonState, } from '../lib/daemon-state.js';
|
|
10
|
-
function readDaemonPid() {
|
|
11
|
-
return readPidFile(DAEMON_PID_FILE);
|
|
12
|
-
}
|
|
13
|
-
function readHealthPid() {
|
|
14
|
-
return readPidFile(HEALTHZ_PID_FILE);
|
|
15
|
-
}
|
|
16
|
-
function killIfAlive(pid, signal = 'SIGTERM') {
|
|
17
|
-
if (!pid)
|
|
18
|
-
return;
|
|
19
|
-
if (!isPidAlive(pid))
|
|
20
|
-
return;
|
|
21
|
-
process.kill(pid, signal);
|
|
22
|
-
}
|
|
23
|
-
function buildHealthSummary(state) {
|
|
24
|
-
const healthPid = readHealthPid();
|
|
25
|
-
const healthRunning = healthPid !== null && isPidAlive(healthPid);
|
|
26
|
-
const port = state?.healthzPort ?? null;
|
|
27
|
-
return {
|
|
28
|
-
configured: port !== null,
|
|
29
|
-
pid: healthRunning ? healthPid : null,
|
|
30
|
-
pidFile: HEALTHZ_PID_FILE,
|
|
31
|
-
port,
|
|
32
|
-
running: healthRunning,
|
|
33
|
-
url: port !== null ? `http://127.0.0.1:${port}/healthz` : null,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function getDaemonStatus() {
|
|
37
|
-
const state = readDaemonState();
|
|
38
|
-
const pid = readDaemonPid();
|
|
39
|
-
const running = pid !== null && isPidAlive(pid);
|
|
40
|
-
return {
|
|
41
|
-
status: running ? 'running' : 'stopped',
|
|
42
|
-
pid: running ? pid : null,
|
|
43
|
-
pidFile: DAEMON_PID_FILE,
|
|
44
|
-
logFile: DAEMON_LOG_FILE,
|
|
45
|
-
stateFile: DAEMON_STATE_FILE,
|
|
46
|
-
health: buildHealthSummary(state),
|
|
47
|
-
lastReloadAt: state?.lastReloadAt ?? null,
|
|
48
|
-
lastReloadStatus: state?.lastReloadStatus ?? null,
|
|
49
|
-
lastReloadMessage: state?.lastReloadMessage ?? null,
|
|
50
|
-
startedAt: state?.startedAt ?? null,
|
|
51
|
-
stoppedAt: state?.stoppedAt ?? null,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
function persistState(partial) {
|
|
55
|
-
const previous = readDaemonState();
|
|
56
|
-
const next = {
|
|
57
|
-
...(previous ?? {}),
|
|
58
|
-
status: partial.status ?? previous?.status ?? 'stopped',
|
|
59
|
-
pid: partial.pid ?? previous?.pid ?? null,
|
|
60
|
-
...partial,
|
|
61
|
-
logFile: DAEMON_LOG_FILE,
|
|
62
|
-
pidFile: DAEMON_PID_FILE,
|
|
63
|
-
stateFile: DAEMON_STATE_FILE,
|
|
64
|
-
};
|
|
65
|
-
writeDaemonState(next);
|
|
66
|
-
return next;
|
|
67
|
-
}
|
|
68
|
-
function readLastLines(filePath, n = 20) {
|
|
69
|
-
try {
|
|
70
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
71
|
-
const lines = content.split('\n');
|
|
72
|
-
return lines.slice(Math.max(0, lines.length - n)).join('\n').trim();
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return '';
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function probeLiveness(opts) {
|
|
79
|
-
await new Promise((resolve) => setTimeout(resolve, opts.delayMs));
|
|
80
|
-
const dead = opts.child.exitCode !== null || opts.child.killed;
|
|
81
|
-
if (!dead)
|
|
82
|
-
return true;
|
|
83
|
-
if (opts.fatal) {
|
|
84
|
-
if (opts.pidFile)
|
|
85
|
-
clearPidFile(opts.pidFile);
|
|
86
|
-
const exitCode = opts.child.exitCode ?? 'unknown';
|
|
87
|
-
const logSnippet = opts.logFile ? readLastLines(opts.logFile) : '';
|
|
88
|
-
const trailingLog = logSnippet ? `\n\nLast log lines:\n${logSnippet}` : '';
|
|
89
|
-
const logRef = opts.logFile ?? 'daemon log';
|
|
90
|
-
persistState({
|
|
91
|
-
status: 'failed', pid: null, failedAt: new Date().toISOString(),
|
|
92
|
-
failureReason: `Daemon exited immediately (code ${exitCode}). Check ${logRef}.`,
|
|
93
|
-
});
|
|
94
|
-
exitWithError({
|
|
95
|
-
code: 1, kind: 'runtime',
|
|
96
|
-
message: `Daemon process exited immediately (code ${exitCode}). Check ${logRef} for details.${trailingLog}`,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
function renderHumanStatus(status) {
|
|
102
|
-
if (status.status === 'running' && status.pid !== null) {
|
|
103
|
-
console.log(`${chalk.green('●')} Daemon is running (pid ${status.pid}).`);
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
console.log(`${chalk.grey('○')} Daemon is not running.`);
|
|
107
|
-
}
|
|
108
|
-
if (status.startedAt)
|
|
109
|
-
console.log(` Started: ${status.startedAt}`);
|
|
110
|
-
if (status.stoppedAt)
|
|
111
|
-
console.log(` Stopped: ${status.stoppedAt}`);
|
|
112
|
-
console.log(` Log: ${status.logFile}`);
|
|
113
|
-
console.log(` PID: ${status.pidFile}`);
|
|
114
|
-
console.log(` State: ${status.stateFile}`);
|
|
115
|
-
if (status.lastReloadAt) {
|
|
116
|
-
console.log(` Reload: ${status.lastReloadAt} (${status.lastReloadStatus ?? 'unknown'})`);
|
|
117
|
-
if (status.lastReloadMessage)
|
|
118
|
-
console.log(` ${status.lastReloadMessage}`);
|
|
119
|
-
}
|
|
120
|
-
if (status.health.configured) {
|
|
121
|
-
if (status.health.running) {
|
|
122
|
-
console.log(`${chalk.green('✓')} Health server running (pid ${status.health.pid})`);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
console.log(`${chalk.yellow('!')} Health server configured but not running`);
|
|
126
|
-
}
|
|
127
|
-
if (status.health.url)
|
|
128
|
-
console.log(` Health: ${status.health.url}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
export function registerDaemonCommand(program) {
|
|
132
|
-
const daemon = program
|
|
133
|
-
.command('daemon')
|
|
134
|
-
.description('Manage the background SwitchBot rules daemon and its health endpoint.')
|
|
135
|
-
.addHelpText('after', `
|
|
136
|
-
The daemon runs \`switchbot rules run\` as a detached background process,
|
|
137
|
-
tracks runtime metadata in ~/.switchbot/daemon.state.json, and can optionally
|
|
138
|
-
co-launch a health endpoint via \`switchbot health serve\`.
|
|
139
|
-
|
|
140
|
-
Subcommands:
|
|
141
|
-
start [--policy <path>] Start the daemon (no-op if already running).
|
|
142
|
-
stop Stop the daemon and any co-launched health server.
|
|
143
|
-
status Report the daemon state, log path, and health summary.
|
|
144
|
-
reload Trigger a hot reload for the running rules engine.
|
|
145
|
-
|
|
146
|
-
The daemon reads the same policy file as \`switchbot rules run\`.
|
|
147
|
-
`);
|
|
148
|
-
daemon
|
|
149
|
-
.command('start')
|
|
150
|
-
.description('Start the rules-engine daemon in the background.')
|
|
151
|
-
.option('--policy <path>', 'Policy file path (default: auto-detected)', stringArg('--policy'))
|
|
152
|
-
.option('--force', 'Restart even if the daemon appears to be running.')
|
|
153
|
-
.option('--healthz-port <n>', 'Also start a health HTTP server on this port (default: disabled).')
|
|
154
|
-
.action(async (opts) => {
|
|
155
|
-
const current = getDaemonStatus();
|
|
156
|
-
if (current.status === 'running' && !opts.force) {
|
|
157
|
-
if (isJsonMode()) {
|
|
158
|
-
printJson({ result: 'already-running', ...current });
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
console.log(`Daemon is already running (pid ${current.pid}). Use --force to restart.`);
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (opts.force) {
|
|
166
|
-
try {
|
|
167
|
-
killIfAlive(current.pid);
|
|
168
|
-
killIfAlive(current.health.pid);
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// best effort
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
175
|
-
const cliEntry = path.resolve(path.dirname(thisFile), '..', 'index.js');
|
|
176
|
-
const args = ['rules', 'run'];
|
|
177
|
-
if (opts.policy)
|
|
178
|
-
args.push(opts.policy);
|
|
179
|
-
fs.mkdirSync(path.dirname(DAEMON_PID_FILE), { recursive: true, mode: 0o700 });
|
|
180
|
-
persistState({
|
|
181
|
-
status: 'starting',
|
|
182
|
-
pid: null,
|
|
183
|
-
startedAt: new Date().toISOString(),
|
|
184
|
-
stoppedAt: undefined,
|
|
185
|
-
failedAt: undefined,
|
|
186
|
-
failureReason: undefined,
|
|
187
|
-
healthzPort: opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null,
|
|
188
|
-
healthzPid: null,
|
|
189
|
-
healthzPidFile: HEALTHZ_PID_FILE,
|
|
190
|
-
});
|
|
191
|
-
const logFd = fs.openSync(DAEMON_LOG_FILE, 'a');
|
|
192
|
-
const child = spawn(process.execPath, [cliEntry, ...args], {
|
|
193
|
-
detached: true,
|
|
194
|
-
stdio: ['ignore', logFd, logFd],
|
|
195
|
-
env: { ...process.env },
|
|
196
|
-
});
|
|
197
|
-
child.unref();
|
|
198
|
-
fs.closeSync(logFd);
|
|
199
|
-
// Liveness probe: wait 300 ms then verify the child is still alive.
|
|
200
|
-
await probeLiveness({
|
|
201
|
-
child, delayMs: 300, fatal: true,
|
|
202
|
-
pidFile: DAEMON_PID_FILE, logFile: DAEMON_LOG_FILE,
|
|
203
|
-
});
|
|
204
|
-
const newPid = child.pid;
|
|
205
|
-
if (!newPid) {
|
|
206
|
-
persistState({
|
|
207
|
-
status: 'failed',
|
|
208
|
-
pid: null,
|
|
209
|
-
failedAt: new Date().toISOString(),
|
|
210
|
-
failureReason: 'Failed to spawn daemon process.',
|
|
211
|
-
});
|
|
212
|
-
exitWithError({ code: 1, kind: 'runtime', message: 'Failed to spawn daemon process.' });
|
|
213
|
-
}
|
|
214
|
-
writePidFile(DAEMON_PID_FILE, newPid);
|
|
215
|
-
let healthzPid = null;
|
|
216
|
-
let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
|
|
217
|
-
if (healthzPort !== null) {
|
|
218
|
-
const healthArgs = ['health', 'serve', '--port', String(healthzPort)];
|
|
219
|
-
const healthLogFd = fs.openSync(DAEMON_LOG_FILE, 'a');
|
|
220
|
-
const healthChild = spawn(process.execPath, [cliEntry, ...healthArgs], {
|
|
221
|
-
detached: true,
|
|
222
|
-
stdio: ['ignore', healthLogFd, healthLogFd],
|
|
223
|
-
env: { ...process.env },
|
|
224
|
-
});
|
|
225
|
-
healthChild.unref();
|
|
226
|
-
fs.closeSync(healthLogFd);
|
|
227
|
-
if (healthChild.pid) {
|
|
228
|
-
// Brief liveness probe for the health server process.
|
|
229
|
-
const healthAlive = await probeLiveness({ child: healthChild, delayMs: 200, fatal: false });
|
|
230
|
-
if (healthAlive) {
|
|
231
|
-
healthzPid = healthChild.pid;
|
|
232
|
-
writePidFile(HEALTHZ_PID_FILE, healthzPid);
|
|
233
|
-
}
|
|
234
|
-
// Non-fatal if health server dies — daemon itself is still running.
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
persistState({
|
|
238
|
-
status: 'running',
|
|
239
|
-
pid: newPid,
|
|
240
|
-
startedAt: new Date().toISOString(),
|
|
241
|
-
stoppedAt: undefined,
|
|
242
|
-
failedAt: undefined,
|
|
243
|
-
failureReason: undefined,
|
|
244
|
-
healthzPort,
|
|
245
|
-
healthzPid,
|
|
246
|
-
healthzPidFile: HEALTHZ_PID_FILE,
|
|
247
|
-
});
|
|
248
|
-
const status = getDaemonStatus();
|
|
249
|
-
if (isJsonMode()) {
|
|
250
|
-
printJson({ result: 'started', ...status });
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
console.log(`${chalk.green('✓')} Daemon started (pid ${newPid})`);
|
|
254
|
-
console.log(` Log: ${DAEMON_LOG_FILE}`);
|
|
255
|
-
console.log(` PID: ${DAEMON_PID_FILE}`);
|
|
256
|
-
console.log(` State: ${DAEMON_STATE_FILE}`);
|
|
257
|
-
console.log(` Reload: switchbot daemon reload`);
|
|
258
|
-
if (status.health.running && status.health.url) {
|
|
259
|
-
console.log(`${chalk.green('✓')} Health server started (pid ${status.health.pid})`);
|
|
260
|
-
console.log(` Health: ${status.health.url}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
daemon
|
|
265
|
-
.command('stop')
|
|
266
|
-
.description('Stop the background daemon by sending SIGTERM.')
|
|
267
|
-
.action(() => {
|
|
268
|
-
const status = getDaemonStatus();
|
|
269
|
-
if (status.status !== 'running' || status.pid === null) {
|
|
270
|
-
persistState({ status: 'stopped', pid: null, stoppedAt: new Date().toISOString() });
|
|
271
|
-
if (isJsonMode()) {
|
|
272
|
-
printJson({ result: 'not-running', ...getDaemonStatus() });
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
console.log(`No running daemon found (pid file: ${DAEMON_PID_FILE}).`);
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
killIfAlive(status.pid);
|
|
281
|
-
killIfAlive(status.health.pid);
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
persistState({
|
|
285
|
-
status: 'failed',
|
|
286
|
-
pid: status.pid,
|
|
287
|
-
failedAt: new Date().toISOString(),
|
|
288
|
-
failureReason: err instanceof Error ? err.message : String(err),
|
|
289
|
-
});
|
|
290
|
-
exitWithError({
|
|
291
|
-
code: 1,
|
|
292
|
-
kind: 'runtime',
|
|
293
|
-
message: `Failed to stop daemon (pid ${status.pid}): ${err instanceof Error ? err.message : String(err)}`,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
try {
|
|
297
|
-
fs.unlinkSync(DAEMON_PID_FILE);
|
|
298
|
-
}
|
|
299
|
-
catch { /* best effort */ }
|
|
300
|
-
try {
|
|
301
|
-
fs.unlinkSync(HEALTHZ_PID_FILE);
|
|
302
|
-
}
|
|
303
|
-
catch { /* best effort */ }
|
|
304
|
-
persistState({
|
|
305
|
-
status: 'stopped',
|
|
306
|
-
pid: null,
|
|
307
|
-
healthzPid: null,
|
|
308
|
-
stoppedAt: new Date().toISOString(),
|
|
309
|
-
});
|
|
310
|
-
if (isJsonMode()) {
|
|
311
|
-
printJson({ result: 'stopped', ...getDaemonStatus() });
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
console.log(`${chalk.green('✓')} Daemon stopped (pid ${status.pid}).`);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
daemon
|
|
318
|
-
.command('status')
|
|
319
|
-
.description('Report whether the daemon is currently running.')
|
|
320
|
-
.action(() => {
|
|
321
|
-
const status = getDaemonStatus();
|
|
322
|
-
if (isJsonMode()) {
|
|
323
|
-
printJson(status);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
renderHumanStatus(status);
|
|
327
|
-
});
|
|
328
|
-
daemon
|
|
329
|
-
.command('reload')
|
|
330
|
-
.description('Trigger a hot reload on the running rules-engine daemon.')
|
|
331
|
-
.action(() => {
|
|
332
|
-
const daemonStatus = getDaemonStatus();
|
|
333
|
-
if (daemonStatus.status !== 'running' || daemonStatus.pid === null) {
|
|
334
|
-
persistState({
|
|
335
|
-
status: 'failed',
|
|
336
|
-
failedAt: new Date().toISOString(),
|
|
337
|
-
failureReason: 'No running daemon to reload.',
|
|
338
|
-
lastReloadAt: new Date().toISOString(),
|
|
339
|
-
lastReloadStatus: 'failed',
|
|
340
|
-
lastReloadMessage: 'No running daemon to reload.',
|
|
341
|
-
});
|
|
342
|
-
exitWithError({
|
|
343
|
-
code: 2,
|
|
344
|
-
kind: 'usage',
|
|
345
|
-
message: `No running daemon found (pid file: ${DAEMON_PID_FILE}).`,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
const pidPaths = getDefaultPidFilePaths();
|
|
349
|
-
const rulesPid = readPidFile(pidPaths.pidFile);
|
|
350
|
-
if (rulesPid === null || !isPidAlive(rulesPid)) {
|
|
351
|
-
persistState({
|
|
352
|
-
status: 'failed',
|
|
353
|
-
failedAt: new Date().toISOString(),
|
|
354
|
-
failureReason: 'Rules engine PID is missing or stale.',
|
|
355
|
-
lastReloadAt: new Date().toISOString(),
|
|
356
|
-
lastReloadStatus: 'failed',
|
|
357
|
-
lastReloadMessage: 'Rules engine PID is missing or stale.',
|
|
358
|
-
});
|
|
359
|
-
exitWithError({
|
|
360
|
-
code: 2,
|
|
361
|
-
kind: 'usage',
|
|
362
|
-
message: `No running rules engine found for daemon reload (pid file: ${pidPaths.pidFile}).`,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
let method;
|
|
366
|
-
try {
|
|
367
|
-
if (sighupSupported()) {
|
|
368
|
-
process.kill(rulesPid, 'SIGHUP');
|
|
369
|
-
method = 'SIGHUP';
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
writeReloadSentinel(pidPaths.reloadFile);
|
|
373
|
-
method = 'sentinel';
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
378
|
-
persistState({
|
|
379
|
-
status: 'failed',
|
|
380
|
-
failedAt: new Date().toISOString(),
|
|
381
|
-
failureReason: message,
|
|
382
|
-
lastReloadAt: new Date().toISOString(),
|
|
383
|
-
lastReloadStatus: 'failed',
|
|
384
|
-
lastReloadMessage: message,
|
|
385
|
-
});
|
|
386
|
-
exitWithError({
|
|
387
|
-
code: 1,
|
|
388
|
-
kind: 'runtime',
|
|
389
|
-
message: `Failed to reload daemon: ${message}`,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
persistState({
|
|
393
|
-
status: 'running',
|
|
394
|
-
lastReloadAt: new Date().toISOString(),
|
|
395
|
-
lastReloadStatus: 'ok',
|
|
396
|
-
lastReloadMessage: method === 'SIGHUP'
|
|
397
|
-
? `Sent SIGHUP to rules engine pid ${rulesPid}.`
|
|
398
|
-
: `Wrote reload sentinel ${pidPaths.reloadFile}.`,
|
|
399
|
-
});
|
|
400
|
-
const status = getDaemonStatus();
|
|
401
|
-
if (isJsonMode()) {
|
|
402
|
-
printJson({ result: 'reloaded', method, ...status });
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
console.log(`${chalk.green('✓')} Reload requested via ${method}.`);
|
|
406
|
-
if (status.lastReloadMessage)
|
|
407
|
-
console.log(` ${status.lastReloadMessage}`);
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { stringArg } from '../utils/arg-parsers.js';
|
|
2
|
-
import { handleError, isJsonMode, printJson, printTable, UsageError } from '../utils/output.js';
|
|
3
|
-
import { loadDeviceMeta, saveDeviceMeta, setDeviceMeta, clearDeviceMeta, getDeviceMeta, getMetaFilePath, } from '../devices/device-meta.js';
|
|
4
|
-
export function registerDevicesMetaCommand(devices) {
|
|
5
|
-
const meta = devices
|
|
6
|
-
.command('meta')
|
|
7
|
-
.description('Manage local device metadata (alias, hide, notes) stored in ~/.switchbot/device-meta.json');
|
|
8
|
-
// switchbot devices meta set <deviceId>
|
|
9
|
-
meta
|
|
10
|
-
.command('set')
|
|
11
|
-
.description('Set local metadata for a device (alias, hide/show, notes)')
|
|
12
|
-
.argument('<deviceId>', 'Target device ID')
|
|
13
|
-
.option('--alias <name>', 'Local alias for the device (used with --name flag)', stringArg('--alias'))
|
|
14
|
-
.option('--hide', 'Hide this device from "devices list"')
|
|
15
|
-
.option('--show', 'Un-hide this device')
|
|
16
|
-
.option('--notes <text>', 'Freeform notes shown in "devices describe"', stringArg('--notes'))
|
|
17
|
-
.option('--force', 'Reassign alias even if it already belongs to another device')
|
|
18
|
-
.action((deviceId, options) => {
|
|
19
|
-
try {
|
|
20
|
-
if (options.hide && options.show) {
|
|
21
|
-
throw new UsageError('--hide and --show cannot be used together.');
|
|
22
|
-
}
|
|
23
|
-
if (!options.alias && !options.hide && !options.show && !options.notes) {
|
|
24
|
-
throw new UsageError('Specify at least one of: --alias, --hide, --show, --notes');
|
|
25
|
-
}
|
|
26
|
-
// Enforce alias uniqueness across devices
|
|
27
|
-
if (options.alias !== undefined) {
|
|
28
|
-
const meta = loadDeviceMeta();
|
|
29
|
-
const holder = Object.entries(meta.devices).find(([id, m]) => m.alias === options.alias && id !== deviceId);
|
|
30
|
-
if (holder) {
|
|
31
|
-
if (!options.force) {
|
|
32
|
-
throw new UsageError(`Alias "${options.alias}" is already assigned to device ${holder[0]}. Use --force to reassign.`);
|
|
33
|
-
}
|
|
34
|
-
// --force: clear the alias from the previous holder
|
|
35
|
-
meta.devices[holder[0]] = { ...meta.devices[holder[0]], alias: undefined };
|
|
36
|
-
saveDeviceMeta(meta);
|
|
37
|
-
if (!isJsonMode()) {
|
|
38
|
-
console.log(`(reassigned alias from ${holder[0]})`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const patch = {};
|
|
43
|
-
if (options.alias !== undefined)
|
|
44
|
-
patch.alias = options.alias;
|
|
45
|
-
if (options.notes !== undefined)
|
|
46
|
-
patch.notes = options.notes;
|
|
47
|
-
if (options.hide)
|
|
48
|
-
patch.hidden = true;
|
|
49
|
-
if (options.show)
|
|
50
|
-
patch.hidden = false;
|
|
51
|
-
setDeviceMeta(deviceId, patch);
|
|
52
|
-
const updated = getDeviceMeta(deviceId);
|
|
53
|
-
if (isJsonMode()) {
|
|
54
|
-
printJson({ ok: true, deviceId, meta: updated });
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.log(`✓ Metadata updated for ${deviceId}`);
|
|
58
|
-
if (updated?.alias)
|
|
59
|
-
console.log(` alias: ${updated.alias}`);
|
|
60
|
-
if (updated?.hidden)
|
|
61
|
-
console.log(` hidden: true`);
|
|
62
|
-
if (updated?.notes)
|
|
63
|
-
console.log(` notes: ${updated.notes}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
handleError(error);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
// switchbot devices meta get <deviceId>
|
|
71
|
-
meta
|
|
72
|
-
.command('get')
|
|
73
|
-
.description('Show local metadata for a device')
|
|
74
|
-
.argument('<deviceId>', 'Target device ID')
|
|
75
|
-
.action((deviceId) => {
|
|
76
|
-
try {
|
|
77
|
-
const entry = getDeviceMeta(deviceId);
|
|
78
|
-
if (!entry) {
|
|
79
|
-
if (isJsonMode()) {
|
|
80
|
-
printJson({ deviceId, meta: null });
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
console.log(`No local metadata for ${deviceId}`);
|
|
84
|
-
}
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (isJsonMode()) {
|
|
88
|
-
printJson({ deviceId, meta: entry });
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
if (entry.alias)
|
|
92
|
-
console.log(`alias: ${entry.alias}`);
|
|
93
|
-
if (entry.hidden !== undefined)
|
|
94
|
-
console.log(`hidden: ${entry.hidden}`);
|
|
95
|
-
if (entry.notes)
|
|
96
|
-
console.log(`notes: ${entry.notes}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
handleError(error);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
// switchbot devices meta list
|
|
104
|
-
meta
|
|
105
|
-
.command('list')
|
|
106
|
-
.description('List all devices with local metadata')
|
|
107
|
-
.option('--hidden-only', 'Show only hidden devices')
|
|
108
|
-
.action((options) => {
|
|
109
|
-
try {
|
|
110
|
-
const file = loadDeviceMeta();
|
|
111
|
-
let entries = Object.entries(file.devices);
|
|
112
|
-
if (options.hiddenOnly)
|
|
113
|
-
entries = entries.filter(([, m]) => m.hidden);
|
|
114
|
-
if (entries.length === 0) {
|
|
115
|
-
if (isJsonMode()) {
|
|
116
|
-
printJson([]);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
console.log('No local metadata entries.');
|
|
120
|
-
console.log(`File: ${getMetaFilePath()}`);
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (isJsonMode()) {
|
|
125
|
-
printJson(entries.map(([id, m]) => ({ deviceId: id, ...m })));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const rows = entries.map(([id, m]) => [
|
|
129
|
-
id,
|
|
130
|
-
m.alias ?? '—',
|
|
131
|
-
m.hidden ? 'yes' : '—',
|
|
132
|
-
m.notes ?? '—',
|
|
133
|
-
]);
|
|
134
|
-
printTable(['deviceId', 'alias', 'hidden', 'notes'], rows);
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
handleError(error);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
// switchbot devices meta clear <deviceId>
|
|
141
|
-
meta
|
|
142
|
-
.command('clear')
|
|
143
|
-
.description('Remove all local metadata for a device')
|
|
144
|
-
.argument('<deviceId>', 'Target device ID')
|
|
145
|
-
.action((deviceId) => {
|
|
146
|
-
try {
|
|
147
|
-
clearDeviceMeta(deviceId);
|
|
148
|
-
if (isJsonMode()) {
|
|
149
|
-
printJson({ ok: true, deviceId });
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
console.log(`✓ Metadata cleared for ${deviceId}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
handleError(error);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|