@switchbot/openapi-cli 3.1.0 → 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.
Files changed (113) hide show
  1. package/README.md +34 -42
  2. package/dist/index.js +56945 -169
  3. package/dist/policy/schema/v0.2.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/api/client.js +0 -235
  6. package/dist/auth.js +0 -20
  7. package/dist/commands/agent-bootstrap.js +0 -182
  8. package/dist/commands/auth.js +0 -354
  9. package/dist/commands/batch.js +0 -413
  10. package/dist/commands/cache.js +0 -126
  11. package/dist/commands/capabilities.js +0 -385
  12. package/dist/commands/catalog.js +0 -359
  13. package/dist/commands/completion.js +0 -385
  14. package/dist/commands/config.js +0 -376
  15. package/dist/commands/daemon.js +0 -367
  16. package/dist/commands/device-meta.js +0 -159
  17. package/dist/commands/devices.js +0 -948
  18. package/dist/commands/doctor.js +0 -1015
  19. package/dist/commands/events.js +0 -563
  20. package/dist/commands/expand.js +0 -130
  21. package/dist/commands/explain.js +0 -139
  22. package/dist/commands/health.js +0 -113
  23. package/dist/commands/history.js +0 -320
  24. package/dist/commands/identity.js +0 -59
  25. package/dist/commands/install.js +0 -246
  26. package/dist/commands/mcp.js +0 -2017
  27. package/dist/commands/plan.js +0 -653
  28. package/dist/commands/policy.js +0 -586
  29. package/dist/commands/quota.js +0 -78
  30. package/dist/commands/rules.js +0 -875
  31. package/dist/commands/scenes.js +0 -264
  32. package/dist/commands/schema.js +0 -177
  33. package/dist/commands/status-sync.js +0 -131
  34. package/dist/commands/uninstall.js +0 -237
  35. package/dist/commands/upgrade-check.js +0 -88
  36. package/dist/commands/watch.js +0 -194
  37. package/dist/commands/webhook.js +0 -182
  38. package/dist/config.js +0 -258
  39. package/dist/credentials/backends/file.js +0 -101
  40. package/dist/credentials/backends/linux.js +0 -129
  41. package/dist/credentials/backends/macos.js +0 -129
  42. package/dist/credentials/backends/windows.js +0 -215
  43. package/dist/credentials/keychain.js +0 -88
  44. package/dist/credentials/prime.js +0 -52
  45. package/dist/devices/cache.js +0 -293
  46. package/dist/devices/catalog.js +0 -767
  47. package/dist/devices/device-meta.js +0 -56
  48. package/dist/devices/history-agg.js +0 -138
  49. package/dist/devices/history-query.js +0 -181
  50. package/dist/devices/param-validator.js +0 -433
  51. package/dist/devices/resources.js +0 -270
  52. package/dist/install/default-steps.js +0 -257
  53. package/dist/install/preflight.js +0 -212
  54. package/dist/install/steps.js +0 -67
  55. package/dist/lib/command-keywords.js +0 -17
  56. package/dist/lib/daemon-state.js +0 -46
  57. package/dist/lib/destructive-mode.js +0 -12
  58. package/dist/lib/devices.js +0 -382
  59. package/dist/lib/idempotency.js +0 -106
  60. package/dist/lib/plan-store.js +0 -68
  61. package/dist/lib/request-context.js +0 -12
  62. package/dist/lib/scenes.js +0 -10
  63. package/dist/logger.js +0 -16
  64. package/dist/mcp/device-history.js +0 -145
  65. package/dist/mcp/events-subscription.js +0 -213
  66. package/dist/mqtt/client.js +0 -180
  67. package/dist/mqtt/credential.js +0 -30
  68. package/dist/policy/add-rule.js +0 -124
  69. package/dist/policy/diff.js +0 -91
  70. package/dist/policy/format.js +0 -57
  71. package/dist/policy/load.js +0 -61
  72. package/dist/policy/migrate.js +0 -67
  73. package/dist/policy/schema.js +0 -18
  74. package/dist/policy/validate.js +0 -262
  75. package/dist/rules/action.js +0 -205
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -203
  78. package/dist/rules/cron-scheduler.js +0 -186
  79. package/dist/rules/destructive.js +0 -52
  80. package/dist/rules/engine.js +0 -757
  81. package/dist/rules/matcher.js +0 -230
  82. package/dist/rules/pid-file.js +0 -95
  83. package/dist/rules/quiet-hours.js +0 -45
  84. package/dist/rules/suggest.js +0 -95
  85. package/dist/rules/throttle.js +0 -116
  86. package/dist/rules/types.js +0 -34
  87. package/dist/rules/webhook-listener.js +0 -223
  88. package/dist/rules/webhook-token.js +0 -90
  89. package/dist/schema/field-aliases.js +0 -131
  90. package/dist/sinks/dispatcher.js +0 -12
  91. package/dist/sinks/file.js +0 -19
  92. package/dist/sinks/format.js +0 -56
  93. package/dist/sinks/homeassistant.js +0 -44
  94. package/dist/sinks/openclaw.js +0 -33
  95. package/dist/sinks/stdout.js +0 -5
  96. package/dist/sinks/telegram.js +0 -28
  97. package/dist/sinks/types.js +0 -1
  98. package/dist/sinks/webhook.js +0 -22
  99. package/dist/status-sync/manager.js +0 -268
  100. package/dist/utils/arg-parsers.js +0 -66
  101. package/dist/utils/audit.js +0 -117
  102. package/dist/utils/filter.js +0 -189
  103. package/dist/utils/flags.js +0 -186
  104. package/dist/utils/format.js +0 -117
  105. package/dist/utils/health.js +0 -101
  106. package/dist/utils/help-json.js +0 -54
  107. package/dist/utils/name-resolver.js +0 -137
  108. package/dist/utils/output.js +0 -404
  109. package/dist/utils/quota.js +0 -227
  110. package/dist/utils/redact.js +0 -68
  111. package/dist/utils/retry.js +0 -140
  112. package/dist/utils/string.js +0 -22
  113. package/dist/version.js +0 -4
@@ -1,367 +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, 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 renderHumanStatus(status) {
69
- if (status.status === 'running' && status.pid !== null) {
70
- console.log(`${chalk.green('●')} Daemon is running (pid ${status.pid}).`);
71
- }
72
- else {
73
- console.log(`${chalk.grey('○')} Daemon is not running.`);
74
- }
75
- if (status.startedAt)
76
- console.log(` Started: ${status.startedAt}`);
77
- if (status.stoppedAt)
78
- console.log(` Stopped: ${status.stoppedAt}`);
79
- console.log(` Log: ${status.logFile}`);
80
- console.log(` PID: ${status.pidFile}`);
81
- console.log(` State: ${status.stateFile}`);
82
- if (status.lastReloadAt) {
83
- console.log(` Reload: ${status.lastReloadAt} (${status.lastReloadStatus ?? 'unknown'})`);
84
- if (status.lastReloadMessage)
85
- console.log(` ${status.lastReloadMessage}`);
86
- }
87
- if (status.health.configured) {
88
- if (status.health.running) {
89
- console.log(`${chalk.green('✓')} Health server running (pid ${status.health.pid})`);
90
- }
91
- else {
92
- console.log(`${chalk.yellow('!')} Health server configured but not running`);
93
- }
94
- if (status.health.url)
95
- console.log(` Health: ${status.health.url}`);
96
- }
97
- }
98
- export function registerDaemonCommand(program) {
99
- const daemon = program
100
- .command('daemon')
101
- .description('Manage the background SwitchBot rules daemon and its health endpoint.')
102
- .addHelpText('after', `
103
- The daemon runs \`switchbot rules run\` as a detached background process,
104
- tracks runtime metadata in ~/.switchbot/daemon.state.json, and can optionally
105
- co-launch a health endpoint via \`switchbot health serve\`.
106
-
107
- Subcommands:
108
- start [--policy <path>] Start the daemon (no-op if already running).
109
- stop Stop the daemon and any co-launched health server.
110
- status Report the daemon state, log path, and health summary.
111
- reload Trigger a hot reload for the running rules engine.
112
-
113
- The daemon reads the same policy file as \`switchbot rules run\`.
114
- `);
115
- daemon
116
- .command('start')
117
- .description('Start the rules-engine daemon in the background.')
118
- .option('--policy <path>', 'Policy file path (default: auto-detected)', stringArg('--policy'))
119
- .option('--force', 'Restart even if the daemon appears to be running.')
120
- .option('--healthz-port <n>', 'Also start a health HTTP server on this port (default: disabled).')
121
- .action((opts) => {
122
- const current = getDaemonStatus();
123
- if (current.status === 'running' && !opts.force) {
124
- if (isJsonMode()) {
125
- printJson({ result: 'already-running', ...current });
126
- }
127
- else {
128
- console.log(`Daemon is already running (pid ${current.pid}). Use --force to restart.`);
129
- }
130
- return;
131
- }
132
- if (opts.force) {
133
- try {
134
- killIfAlive(current.pid);
135
- killIfAlive(current.health.pid);
136
- }
137
- catch {
138
- // best effort
139
- }
140
- }
141
- const thisFile = fileURLToPath(import.meta.url);
142
- const cliEntry = path.resolve(path.dirname(thisFile), 'index.js');
143
- const args = ['rules', 'run'];
144
- if (opts.policy)
145
- args.push(opts.policy);
146
- fs.mkdirSync(path.dirname(DAEMON_PID_FILE), { recursive: true, mode: 0o700 });
147
- persistState({
148
- status: 'starting',
149
- pid: null,
150
- startedAt: new Date().toISOString(),
151
- stoppedAt: undefined,
152
- failedAt: undefined,
153
- failureReason: undefined,
154
- healthzPort: opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null,
155
- healthzPid: null,
156
- healthzPidFile: HEALTHZ_PID_FILE,
157
- });
158
- const logFd = fs.openSync(DAEMON_LOG_FILE, 'a');
159
- const child = spawn(process.execPath, [cliEntry, ...args], {
160
- detached: true,
161
- stdio: ['ignore', logFd, logFd],
162
- env: { ...process.env },
163
- });
164
- child.unref();
165
- fs.closeSync(logFd);
166
- const newPid = child.pid;
167
- if (!newPid) {
168
- persistState({
169
- status: 'failed',
170
- pid: null,
171
- failedAt: new Date().toISOString(),
172
- failureReason: 'Failed to spawn daemon process.',
173
- });
174
- exitWithError({ code: 1, kind: 'runtime', message: 'Failed to spawn daemon process.' });
175
- }
176
- writePidFile(DAEMON_PID_FILE, newPid);
177
- let healthzPid = null;
178
- let healthzPort = opts.healthzPort ? Number.parseInt(opts.healthzPort, 10) : null;
179
- if (healthzPort !== null) {
180
- const healthArgs = ['health', 'serve', '--port', String(healthzPort)];
181
- const healthLogFd = fs.openSync(DAEMON_LOG_FILE, 'a');
182
- const healthChild = spawn(process.execPath, [cliEntry, ...healthArgs], {
183
- detached: true,
184
- stdio: ['ignore', healthLogFd, healthLogFd],
185
- env: { ...process.env },
186
- });
187
- healthChild.unref();
188
- fs.closeSync(healthLogFd);
189
- if (healthChild.pid) {
190
- healthzPid = healthChild.pid;
191
- writePidFile(HEALTHZ_PID_FILE, healthzPid);
192
- }
193
- }
194
- persistState({
195
- status: 'running',
196
- pid: newPid,
197
- startedAt: new Date().toISOString(),
198
- stoppedAt: undefined,
199
- failedAt: undefined,
200
- failureReason: undefined,
201
- healthzPort,
202
- healthzPid,
203
- healthzPidFile: HEALTHZ_PID_FILE,
204
- });
205
- const status = getDaemonStatus();
206
- if (isJsonMode()) {
207
- printJson({ result: 'started', ...status });
208
- }
209
- else {
210
- console.log(`${chalk.green('✓')} Daemon started (pid ${newPid})`);
211
- console.log(` Log: ${DAEMON_LOG_FILE}`);
212
- console.log(` PID: ${DAEMON_PID_FILE}`);
213
- console.log(` State: ${DAEMON_STATE_FILE}`);
214
- console.log(` Reload: switchbot daemon reload`);
215
- if (status.health.running && status.health.url) {
216
- console.log(`${chalk.green('✓')} Health server started (pid ${status.health.pid})`);
217
- console.log(` Health: ${status.health.url}`);
218
- }
219
- }
220
- });
221
- daemon
222
- .command('stop')
223
- .description('Stop the background daemon by sending SIGTERM.')
224
- .action(() => {
225
- const status = getDaemonStatus();
226
- if (status.status !== 'running' || status.pid === null) {
227
- persistState({ status: 'stopped', pid: null, stoppedAt: new Date().toISOString() });
228
- if (isJsonMode()) {
229
- printJson({ result: 'not-running', ...getDaemonStatus() });
230
- }
231
- else {
232
- console.log(`No running daemon found (pid file: ${DAEMON_PID_FILE}).`);
233
- }
234
- return;
235
- }
236
- try {
237
- killIfAlive(status.pid);
238
- killIfAlive(status.health.pid);
239
- }
240
- catch (err) {
241
- persistState({
242
- status: 'failed',
243
- pid: status.pid,
244
- failedAt: new Date().toISOString(),
245
- failureReason: err instanceof Error ? err.message : String(err),
246
- });
247
- exitWithError({
248
- code: 1,
249
- kind: 'runtime',
250
- message: `Failed to stop daemon (pid ${status.pid}): ${err instanceof Error ? err.message : String(err)}`,
251
- });
252
- }
253
- try {
254
- fs.unlinkSync(DAEMON_PID_FILE);
255
- }
256
- catch { /* best effort */ }
257
- try {
258
- fs.unlinkSync(HEALTHZ_PID_FILE);
259
- }
260
- catch { /* best effort */ }
261
- persistState({
262
- status: 'stopped',
263
- pid: null,
264
- healthzPid: null,
265
- stoppedAt: new Date().toISOString(),
266
- });
267
- if (isJsonMode()) {
268
- printJson({ result: 'stopped', ...getDaemonStatus() });
269
- }
270
- else {
271
- console.log(`${chalk.green('✓')} Daemon stopped (pid ${status.pid}).`);
272
- }
273
- });
274
- daemon
275
- .command('status')
276
- .description('Report whether the daemon is currently running.')
277
- .action(() => {
278
- const status = getDaemonStatus();
279
- if (isJsonMode()) {
280
- printJson(status);
281
- return;
282
- }
283
- renderHumanStatus(status);
284
- });
285
- daemon
286
- .command('reload')
287
- .description('Trigger a hot reload on the running rules-engine daemon.')
288
- .action(() => {
289
- const daemonStatus = getDaemonStatus();
290
- if (daemonStatus.status !== 'running' || daemonStatus.pid === null) {
291
- persistState({
292
- status: 'failed',
293
- failedAt: new Date().toISOString(),
294
- failureReason: 'No running daemon to reload.',
295
- lastReloadAt: new Date().toISOString(),
296
- lastReloadStatus: 'failed',
297
- lastReloadMessage: 'No running daemon to reload.',
298
- });
299
- exitWithError({
300
- code: 2,
301
- kind: 'usage',
302
- message: `No running daemon found (pid file: ${DAEMON_PID_FILE}).`,
303
- });
304
- }
305
- const pidPaths = getDefaultPidFilePaths();
306
- const rulesPid = readPidFile(pidPaths.pidFile);
307
- if (rulesPid === null || !isPidAlive(rulesPid)) {
308
- persistState({
309
- status: 'failed',
310
- failedAt: new Date().toISOString(),
311
- failureReason: 'Rules engine PID is missing or stale.',
312
- lastReloadAt: new Date().toISOString(),
313
- lastReloadStatus: 'failed',
314
- lastReloadMessage: 'Rules engine PID is missing or stale.',
315
- });
316
- exitWithError({
317
- code: 2,
318
- kind: 'usage',
319
- message: `No running rules engine found for daemon reload (pid file: ${pidPaths.pidFile}).`,
320
- });
321
- }
322
- let method;
323
- try {
324
- if (sighupSupported()) {
325
- process.kill(rulesPid, 'SIGHUP');
326
- method = 'SIGHUP';
327
- }
328
- else {
329
- writeReloadSentinel(pidPaths.reloadFile);
330
- method = 'sentinel';
331
- }
332
- }
333
- catch (err) {
334
- const message = err instanceof Error ? err.message : String(err);
335
- persistState({
336
- status: 'failed',
337
- failedAt: new Date().toISOString(),
338
- failureReason: message,
339
- lastReloadAt: new Date().toISOString(),
340
- lastReloadStatus: 'failed',
341
- lastReloadMessage: message,
342
- });
343
- exitWithError({
344
- code: 1,
345
- kind: 'runtime',
346
- message: `Failed to reload daemon: ${message}`,
347
- });
348
- }
349
- persistState({
350
- status: 'running',
351
- lastReloadAt: new Date().toISOString(),
352
- lastReloadStatus: 'ok',
353
- lastReloadMessage: method === 'SIGHUP'
354
- ? `Sent SIGHUP to rules engine pid ${rulesPid}.`
355
- : `Wrote reload sentinel ${pidPaths.reloadFile}.`,
356
- });
357
- const status = getDaemonStatus();
358
- if (isJsonMode()) {
359
- printJson({ result: 'reloaded', method, ...status });
360
- }
361
- else {
362
- console.log(`${chalk.green('✓')} Reload requested via ${method}.`);
363
- if (status.lastReloadMessage)
364
- console.log(` ${status.lastReloadMessage}`);
365
- }
366
- });
367
- }
@@ -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
- }