@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,413 +0,0 @@
1
- import { intArg, enumArg, stringArg } from '../utils/arg-parsers.js';
2
- import { printJson, isJsonMode, handleError, buildErrorPayload, UsageError, exitWithError } from '../utils/output.js';
3
- import { fetchDeviceList, executeCommand, isDestructiveCommand, buildHubLocationMap, } from '../lib/devices.js';
4
- import { createClient } from '../api/client.js';
5
- import { parseFilter, applyFilter, FilterSyntaxError } from '../utils/filter.js';
6
- import { isDryRun } from '../utils/flags.js';
7
- import { DryRunSignal } from '../api/client.js';
8
- import { getCachedTypeMap, getCachedDevice, loadStatusCache } from '../devices/cache.js';
9
- import { allowsDirectDestructiveExecution, destructiveExecutionHint } from '../lib/destructive-mode.js';
10
- const DEFAULT_CONCURRENCY = 5;
11
- const COMMAND_TYPES = ['command', 'customize'];
12
- /**
13
- * Run `task(x)` for every element with at most `concurrency` running at once.
14
- * `staggerMs`: when > 0, delay each task start by this fixed interval (replaces
15
- * the default 20-60ms jitter). Useful for rate-limited endpoints.
16
- */
17
- async function runPool(items, concurrency, staggerMs, task) {
18
- const results = new Array(items.length);
19
- let cursor = 0;
20
- const workers = [];
21
- const width = Math.max(1, Math.min(concurrency, items.length));
22
- for (let w = 0; w < width; w++) {
23
- workers.push((async () => {
24
- while (cursor < items.length) {
25
- const idx = cursor++;
26
- results[idx] = await task(items[idx]);
27
- // Fixed stagger wins over random jitter when set; else keep the
28
- // default polite spacing so we don't hammer the endpoint.
29
- const delay = staggerMs > 0 ? staggerMs : 20 + Math.random() * 40;
30
- await new Promise((r) => setTimeout(r, delay));
31
- }
32
- })());
33
- }
34
- await Promise.all(workers);
35
- return results;
36
- }
37
- async function resolveTargetIds(options, getClient) {
38
- const explicit = [];
39
- if (options.ids) {
40
- for (const id of options.ids.split(',').map((s) => s.trim()).filter(Boolean)) {
41
- explicit.push(id);
42
- }
43
- }
44
- if (options.readStdin) {
45
- const chunks = [];
46
- for await (const chunk of process.stdin)
47
- chunks.push(chunk);
48
- const raw = Buffer.concat(chunks).toString('utf-8');
49
- for (const line of raw.split(/\r?\n/)) {
50
- const id = line.trim();
51
- if (id)
52
- explicit.push(id);
53
- }
54
- }
55
- const hasFilter = Boolean(options.filter);
56
- if (explicit.length === 0 && !hasFilter) {
57
- throw new Error('No target devices supplied — provide --ids, --filter, or pass "-" to read deviceIds from stdin.');
58
- }
59
- const typeMap = getCachedTypeMap(explicit);
60
- let ids;
61
- if (hasFilter) {
62
- const body = await fetchDeviceList(getClient?.());
63
- const hubLoc = buildHubLocationMap(body.deviceList);
64
- for (const d of body.deviceList)
65
- if (d.deviceType)
66
- typeMap.set(d.deviceId, d.deviceType);
67
- for (const ir of body.infraredRemoteList)
68
- typeMap.set(ir.deviceId, ir.remoteType);
69
- const clauses = parseFilter(options.filter);
70
- const matched = applyFilter(clauses, body.deviceList, body.infraredRemoteList, hubLoc);
71
- const filteredIds = new Set(matched.map((m) => m.deviceId));
72
- ids =
73
- explicit.length > 0 ? explicit.filter((id) => filteredIds.has(id)) : [...filteredIds];
74
- }
75
- else {
76
- ids = explicit;
77
- const missingTypeInfo = ids.some((id) => !typeMap.has(id));
78
- if (missingTypeInfo) {
79
- const body = await fetchDeviceList(getClient?.());
80
- for (const d of body.deviceList)
81
- if (d.deviceType)
82
- typeMap.set(d.deviceId, d.deviceType);
83
- for (const ir of body.infraredRemoteList)
84
- typeMap.set(ir.deviceId, ir.remoteType);
85
- }
86
- }
87
- return { ids, typeMap };
88
- }
89
- export function registerBatchCommand(devices) {
90
- devices
91
- .command('batch')
92
- .description('Send the same command to many devices in one run (filter- or stdin-driven)')
93
- .argument('<command>', 'Command name, e.g. turnOn, turnOff, setBrightness')
94
- .argument('[parameter]', 'Command parameter (same rules as `devices command`; omit for no-arg)')
95
- .option('--filter <expr>', 'Target devices matching a filter, e.g. type=Bot,family=Home', stringArg('--filter'))
96
- .option('--ids <csv>', 'Explicit comma-separated list of deviceIds', stringArg('--ids'))
97
- .option('--concurrency <n>', 'Max parallel in-flight requests (default 5)', intArg('--concurrency', { min: 1 }), '5')
98
- .option('--max-concurrent <n>', 'Alias for --concurrency; takes priority when set', intArg('--max-concurrent', { min: 1 }))
99
- .option('--stagger <ms>', 'Fixed delay between task starts in ms (default 0 = random 20-60ms jitter)', intArg('--stagger', { min: 0 }), '0')
100
- .option('--plan', '[DEPRECATED, use --emit-plan] With --dry-run: emit a plan JSON document instead of executing anything')
101
- .option('--emit-plan', 'With --dry-run: emit a plan JSON document instead of executing anything')
102
- .option('--yes', 'Allow destructive commands only from an explicit dev profile')
103
- .option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
104
- .option('--stdin', 'Read deviceIds from stdin, one per line (same as trailing "-")')
105
- .option('--idempotency-key-prefix <prefix>', 'Client-supplied prefix for idempotency keys (key per device: <prefix>-<deviceId>). process-local 60s window; cache is per Node process (MCP session, batch run, plan run). Independent CLI invocations do not share cache.', stringArg('--idempotency-key-prefix'))
106
- .option('--idempotency-key <prefix>', 'Alias for --idempotency-key-prefix.', stringArg('--idempotency-key'))
107
- .option('--skip-offline', 'Skip devices whose cached status is offline (no API call; cache miss → send as usual).')
108
- .addHelpText('after', `
109
- Targets are resolved in this priority order:
110
- 1. --ids when present (explicit deviceIds)
111
- 2. stdin when --stdin / "-" (one deviceId per line)
112
- 3. --filter (matches the account's device list)
113
- You can combine explicit ids with --filter to intersect them.
114
-
115
- Filter grammar:
116
- key=value exact match
117
- key~=value case-insensitive substring match
118
- clauses are comma-separated AND
119
-
120
- Supported keys: type, family, room, category (category: physical | ir)
121
-
122
- Output:
123
- Human mode: one status line per device, summary at the end.
124
- --json: {succeeded[], failed[{deviceId,error}], summary:{total,ok,failed,skipped,durationMs,maxConcurrent,staggerMs}}
125
- Each step includes startedAt / finishedAt / durationMs / replayed (when cached).
126
-
127
- Concurrency & pacing:
128
- --max-concurrent <n> Upper bound on in-flight requests (alias for --concurrency).
129
- --stagger <ms> Fixed delay between task starts; default 0 uses random 20-60ms jitter.
130
-
131
- Planning:
132
- --dry-run --emit-plan Print the plan JSON without executing anything. Useful
133
- for agents that want to show the user what will run.
134
- (--plan is the deprecated alias, removed in v3.0.)
135
-
136
- Safety:
137
- Destructive commands (Smart Lock unlock, Garage Door Opener turnOn/turnOff,
138
- Keypad createKey/deleteKey) are blocked by default. Use the reviewed plan
139
- flow instead of direct execution.
140
- --dry-run intercepts every POST and reports the intended calls without
141
- hitting the API.
142
-
143
- Examples:
144
- $ switchbot devices batch turnOff --filter 'type~=Light,family=home'
145
- $ switchbot devices batch turnOn --ids ID1,ID2,ID3
146
- $ switchbot devices list --format=id --filter 'type=Bot' | switchbot devices batch toggle -
147
- $ switchbot devices batch unlock --filter 'type=Smart Lock' --dry-run --emit-plan
148
- `)
149
- .action(async (cmd, parameter, options, commandObj) => {
150
- // Trailing "-" sentinel selects stdin mode.
151
- const extra = commandObj.args ?? [];
152
- const readStdin = Boolean(options.stdin) || extra.includes('-');
153
- // P12: --plan is deprecated in favor of --emit-plan. Reject both
154
- // together (conflicting) and warn when only the old flag is used.
155
- if (options.plan && options.emitPlan) {
156
- handleError(new UsageError('Use --emit-plan; --plan is deprecated and cannot be combined with --emit-plan.'));
157
- return;
158
- }
159
- if (options.plan && !options.emitPlan) {
160
- // Warning goes to stderr so it cannot corrupt --json output on stdout.
161
- console.error('[WARN] --plan is deprecated; use --emit-plan. Will be removed in v3.0.');
162
- }
163
- const emitPlan = Boolean(options.emitPlan || options.plan);
164
- // Accept --idempotency-key as alias; reject when both forms are supplied.
165
- if (options.idempotencyKey !== undefined && options.idempotencyKeyPrefix !== undefined) {
166
- handleError(new UsageError('Use either --idempotency-key or --idempotency-key-prefix, not both.'));
167
- return;
168
- }
169
- if (options.idempotencyKey !== undefined && options.idempotencyKeyPrefix === undefined) {
170
- options.idempotencyKeyPrefix = options.idempotencyKey;
171
- }
172
- let client;
173
- const getClient = () => (client ??= createClient());
174
- let resolved;
175
- try {
176
- resolved = await resolveTargetIds({
177
- filter: options.filter,
178
- ids: options.ids,
179
- readStdin,
180
- }, getClient);
181
- }
182
- catch (error) {
183
- if (error instanceof FilterSyntaxError) {
184
- exitWithError(`Error: ${error.message}`);
185
- }
186
- if (error instanceof Error && error.message.startsWith('No target devices')) {
187
- exitWithError(`Error: ${error.message}`);
188
- }
189
- handleError(error);
190
- }
191
- if (resolved.ids.length === 0) {
192
- const out = {
193
- succeeded: [],
194
- failed: [],
195
- summary: { total: 0, ok: 0, failed: 0, skipped: 0, durationMs: 0, unverifiableCount: 0 },
196
- };
197
- if (isJsonMode())
198
- printJson(out);
199
- else
200
- console.log('No devices matched — nothing to do.');
201
- return;
202
- }
203
- const effectiveType = (options.type === 'customize' ? 'customize' : 'command');
204
- // --skip-offline: preflight using the status cache (no network). Cache
205
- // miss = send as usual; only definite "offline" cached entries skip.
206
- const preSkipped = [];
207
- if (options.skipOffline && resolved.ids.length > 0) {
208
- const statusCache = loadStatusCache();
209
- const kept = [];
210
- for (const id of resolved.ids) {
211
- const entry = statusCache.entries[id];
212
- const online = entry?.body?.onlineStatus;
213
- if (online === 'offline') {
214
- preSkipped.push({ deviceId: id, reason: 'offline' });
215
- }
216
- else {
217
- kept.push(id);
218
- }
219
- }
220
- resolved = { ...resolved, ids: kept };
221
- }
222
- // Pre-flight: identify destructive targets before spending API calls.
223
- const blockedForDestructive = [];
224
- for (const id of resolved.ids) {
225
- const t = resolved.typeMap.get(id);
226
- if (isDestructiveCommand(t, cmd, effectiveType) && !options.yes) {
227
- blockedForDestructive.push({
228
- deviceId: id,
229
- reason: `destructive command "${cmd}" on ${t ?? 'unknown'} requires --yes`,
230
- });
231
- }
232
- }
233
- if (blockedForDestructive.length > 0 && !options.yes) {
234
- const deviceIds = blockedForDestructive.map((b) => b.deviceId);
235
- exitWithError({
236
- code: 2,
237
- kind: 'guard',
238
- message: `Refusing to run destructive command "${cmd}" on ${blockedForDestructive.length} device(s) without --yes: ${deviceIds.join(', ')}`,
239
- hint: 'Re-issue the call with --yes only from an explicit dev profile, or use the reviewed plan flow.',
240
- context: { command: cmd, deviceIds },
241
- });
242
- }
243
- if (blockedForDestructive.length > 0 && options.yes && !allowsDirectDestructiveExecution()) {
244
- exitWithError({
245
- code: 2,
246
- kind: 'guard',
247
- message: `Direct destructive execution is disabled for batch command "${cmd}".`,
248
- hint: destructiveExecutionHint(),
249
- context: {
250
- command: cmd,
251
- blockedCount: blockedForDestructive.length,
252
- deviceIds: blockedForDestructive.map((item) => item.deviceId),
253
- requiredWorkflow: 'plan-approval',
254
- },
255
- });
256
- }
257
- // parameter may be a JSON object string; mirror the single-command action.
258
- let parsedParam = parameter ?? 'default';
259
- if (parameter) {
260
- try {
261
- parsedParam = JSON.parse(parameter);
262
- }
263
- catch {
264
- // keep as string
265
- }
266
- }
267
- const maxConcurrentRaw = options.maxConcurrent ?? options.concurrency;
268
- const concurrency = Math.max(1, Number.parseInt(maxConcurrentRaw, 10) || DEFAULT_CONCURRENCY);
269
- const staggerMs = Math.max(0, Number.parseInt(options.stagger, 10) || 0);
270
- const dryRun = isDryRun();
271
- // --dry-run --emit-plan (or legacy --plan): emit a plan document and return without executing.
272
- if (dryRun && emitPlan) {
273
- const steps = resolved.ids.map((id) => ({
274
- deviceId: id,
275
- command: cmd,
276
- parameter: parsedParam,
277
- type: effectiveType,
278
- idempotencyKey: options.idempotencyKeyPrefix
279
- ? `${options.idempotencyKeyPrefix}-${id}`
280
- : undefined,
281
- }));
282
- const planDoc = {
283
- schemaVersion: '1.1',
284
- dryRun: true,
285
- plan: {
286
- command: cmd,
287
- parameter: parsedParam,
288
- type: effectiveType,
289
- maxConcurrent: concurrency,
290
- staggerMs,
291
- stepCount: steps.length,
292
- steps,
293
- },
294
- };
295
- if (isJsonMode()) {
296
- printJson(planDoc);
297
- }
298
- else {
299
- console.log(`Plan: ${steps.length} step(s), command=${cmd}, maxConcurrent=${concurrency}, staggerMs=${staggerMs}`);
300
- for (const s of steps)
301
- console.log(` → ${s.deviceId} ${s.type} ${s.command}`);
302
- }
303
- return;
304
- }
305
- const startedAt = Date.now();
306
- const outcomes = await runPool(resolved.ids, concurrency, staggerMs, async (id) => {
307
- const stepStart = Date.now();
308
- const startedIso = new Date(stepStart).toISOString();
309
- try {
310
- const idempotencyKey = options.idempotencyKeyPrefix
311
- ? `${options.idempotencyKeyPrefix}-${id}`
312
- : undefined;
313
- const result = await executeCommand(id, cmd, parsedParam, effectiveType, getClient(), {
314
- idempotencyKey,
315
- });
316
- const finishedIso = new Date().toISOString();
317
- const durationMs = Date.now() - stepStart;
318
- const replayed = typeof result === 'object' && result !== null && result.replayed === true;
319
- if (!isJsonMode()) {
320
- console.log(`✓ ${id}: ${cmd}${replayed ? ' (replayed)' : ''}`);
321
- }
322
- return {
323
- ok: true,
324
- deviceId: id,
325
- result,
326
- startedAt: startedIso,
327
- finishedAt: finishedIso,
328
- durationMs,
329
- replayed,
330
- };
331
- }
332
- catch (err) {
333
- // --dry-run uses DryRunSignal to short-circuit; surface that as a
334
- // "skipped" outcome, not a failure.
335
- if (err instanceof DryRunSignal) {
336
- return {
337
- ok: 'dry-run',
338
- deviceId: id,
339
- startedAt: startedIso,
340
- finishedAt: new Date().toISOString(),
341
- durationMs: Date.now() - stepStart,
342
- };
343
- }
344
- const errorPayload = buildErrorPayload(err);
345
- if (!isJsonMode()) {
346
- console.error(`✗ ${id}: ${errorPayload.message}`);
347
- }
348
- return {
349
- ok: false,
350
- deviceId: id,
351
- error: errorPayload,
352
- startedAt: startedIso,
353
- finishedAt: new Date().toISOString(),
354
- durationMs: Date.now() - stepStart,
355
- };
356
- }
357
- });
358
- const succeeded = outcomes.filter((o) => o.ok === true);
359
- const failed = outcomes.filter((o) => o.ok === false);
360
- const dryRunned = outcomes.filter((o) => o.ok === 'dry-run');
361
- const result = {
362
- succeeded: succeeded.map((s) => {
363
- const isIr = getCachedDevice(s.deviceId)?.category === 'ir';
364
- const entry = {
365
- deviceId: s.deviceId,
366
- result: s.result,
367
- startedAt: s.startedAt,
368
- finishedAt: s.finishedAt,
369
- durationMs: s.durationMs,
370
- replayed: s.replayed,
371
- };
372
- if (isIr) {
373
- entry.subKind = 'ir-no-feedback';
374
- entry.verification = {
375
- verifiable: false,
376
- reason: 'IR transmission is unidirectional; no receipt acknowledgment is possible.',
377
- suggestedFollowup: 'Confirm visible change manually or via a paired state sensor.',
378
- };
379
- }
380
- return entry;
381
- }),
382
- failed: failed.map((f) => ({
383
- deviceId: f.deviceId,
384
- error: f.error,
385
- startedAt: f.startedAt,
386
- finishedAt: f.finishedAt,
387
- durationMs: f.durationMs,
388
- })),
389
- ...(preSkipped.length > 0 ? { skipped: preSkipped } : {}),
390
- summary: {
391
- total: resolved.ids.length + preSkipped.length,
392
- ok: succeeded.length,
393
- failed: failed.length,
394
- skipped: dryRunned.length + preSkipped.length,
395
- durationMs: Date.now() - startedAt,
396
- unverifiableCount: succeeded.filter((s) => getCachedDevice(s.deviceId)?.category === 'ir').length,
397
- schemaVersion: '1.1',
398
- maxConcurrent: concurrency,
399
- staggerMs,
400
- ...(dryRun ? { dryRun: true } : {}),
401
- },
402
- };
403
- if (isJsonMode()) {
404
- printJson(result);
405
- }
406
- else {
407
- console.log(`\nSummary: ${result.summary.ok} ok, ${result.summary.failed} failed, ${result.summary.skipped} skipped (${result.summary.durationMs}ms)`);
408
- }
409
- // Non-zero exit when anything failed so scripts can react.
410
- if (failed.length > 0)
411
- process.exit(1);
412
- });
413
- }
@@ -1,126 +0,0 @@
1
- import { enumArg } from '../utils/arg-parsers.js';
2
- import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
3
- import { clearCache, clearStatusCache, describeCache, loadStatusCache, } from '../devices/cache.js';
4
- function formatAge(ms) {
5
- if (ms === undefined)
6
- return '—';
7
- if (ms < 1000)
8
- return `${ms}ms`;
9
- const s = Math.floor(ms / 1000);
10
- if (s < 60)
11
- return `${s}s`;
12
- const m = Math.floor(s / 60);
13
- if (m < 60)
14
- return `${m}m ${s % 60}s`;
15
- const h = Math.floor(m / 60);
16
- return `${h}h ${m % 60}m`;
17
- }
18
- export function registerCacheCommand(program) {
19
- const CACHE_KEYS = ['list', 'status', 'all'];
20
- const cache = program
21
- .command('cache')
22
- .description('Inspect and manage the local SwitchBot CLI caches')
23
- .addHelpText('after', `
24
- Two caches live at ~/.switchbot/:
25
- devices.json List of known deviceIds + metadata. Refreshed by every
26
- 'devices list' call. Drives command validation and
27
- helpful hints — keep around even if you don't use TTL.
28
- status.json Per-device status bodies keyed by deviceId. Only written
29
- when a status TTL is enabled (via --cache <duration>).
30
-
31
- Cache modes (global flag, apply to any read):
32
- --cache off | --no-cache disable cache reads
33
- --cache auto (default) list cache on (1h TTL), status cache off
34
- --cache 5m | --cache 1h enable both with the given TTL
35
-
36
- Subcommands:
37
- show Show ages, entry counts, and file locations
38
- clear Delete cache files (specify --key to scope)
39
-
40
- Examples:
41
- $ switchbot cache show
42
- $ switchbot --json cache show
43
- $ switchbot cache clear # removes devices.json + status.json
44
- $ switchbot cache clear --key status # removes only status.json
45
- $ switchbot cache clear --key list # removes only devices.json
46
- `);
47
- cache
48
- .command('show')
49
- .alias('status')
50
- .description('Summarize the cache files (paths, ages, entry counts)')
51
- .addHelpText('after', `
52
- Cache TTL is computed from the 'lastUpdated' field inside the JSON, not the file mtime.
53
- touch does not invalidate; use 'cache clear' to force a refresh.
54
- `)
55
- .action(() => {
56
- const summary = describeCache();
57
- if (isJsonMode()) {
58
- const statusCache = loadStatusCache();
59
- printJson({
60
- list: summary.list,
61
- status: {
62
- ...summary.status,
63
- entries: Object.fromEntries(Object.entries(statusCache.entries).map(([id, e]) => [id, { fetchedAt: e.fetchedAt }])),
64
- },
65
- });
66
- return;
67
- }
68
- console.log('Device list cache (devices.json):');
69
- console.log(` Path: ${summary.list.path}`);
70
- console.log(` Exists: ${summary.list.exists ? 'yes' : 'no'}`);
71
- if (summary.list.exists) {
72
- console.log(` Last update: ${summary.list.lastUpdated ?? '—'}`);
73
- console.log(` Age: ${formatAge(summary.list.ageMs)}`);
74
- console.log(` Devices: ${summary.list.deviceCount ?? 0}`);
75
- }
76
- console.log('\nStatus cache (status.json):');
77
- console.log(` Path: ${summary.status.path}`);
78
- console.log(` Exists: ${summary.status.exists ? 'yes' : 'no'}`);
79
- console.log(` Entries: ${summary.status.entryCount}`);
80
- if (summary.status.entryCount > 0) {
81
- console.log(` Oldest: ${summary.status.oldestFetchedAt ?? '—'}`);
82
- console.log(` Newest: ${summary.status.newestFetchedAt ?? '—'}`);
83
- }
84
- });
85
- cache
86
- .command('clear')
87
- .description('Delete cache files')
88
- .option('--key <which>', 'Which cache to clear: "list" | "status" | "all" (default)', enumArg('--key', CACHE_KEYS), 'all')
89
- .option('--status', 'Shorthand for --key status')
90
- .option('--list', 'Shorthand for --key list')
91
- .action((options) => {
92
- try {
93
- if (options.status && options.list) {
94
- throw new UsageError('--status and --list are mutually exclusive.');
95
- }
96
- if ((options.status || options.list) && options.key !== 'all') {
97
- throw new UsageError('--status / --list cannot be combined with --key.');
98
- }
99
- let key = options.key;
100
- if (options.status)
101
- key = 'status';
102
- if (options.list)
103
- key = 'list';
104
- if (!['list', 'status', 'all'].includes(key)) {
105
- throw new UsageError(`Unknown --key "${key}". Expected: list, status, all.`);
106
- }
107
- const cleared = [];
108
- if (key === 'list' || key === 'all') {
109
- clearCache();
110
- cleared.push('list');
111
- }
112
- if (key === 'status' || key === 'all') {
113
- clearStatusCache();
114
- cleared.push('status');
115
- }
116
- if (isJsonMode()) {
117
- printJson({ cleared });
118
- return;
119
- }
120
- console.log(`Cleared: ${cleared.join(', ')}`);
121
- }
122
- catch (error) {
123
- handleError(error);
124
- }
125
- });
126
- }