@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.
- package/README.md +34 -42
- 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 -367
- 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 -88
- 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 -205
- package/dist/rules/audit-query.js +0 -89
- package/dist/rules/conflict-analyzer.js +0 -203
- 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 -117
- 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/devices/cache.js
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import { getConfigPath } from '../utils/flags.js';
|
|
6
|
-
import { getActiveProfile } from '../lib/request-context.js';
|
|
7
|
-
/**
|
|
8
|
-
* Returns the directory where cache files should be stored.
|
|
9
|
-
*
|
|
10
|
-
* - If a profile is active, scopes into a per-profile sub-directory so that
|
|
11
|
-
* rotating credentials or switching profiles never serves stale inventory
|
|
12
|
-
* from a prior session (Bug #37).
|
|
13
|
-
* - If no profile is active (unnamed / default), returns `baseDir` unchanged
|
|
14
|
-
* so the existing legacy path (~/.switchbot/devices.json) is preserved.
|
|
15
|
-
*
|
|
16
|
-
* Only called when `getConfigPath()` returns undefined — the --config-path
|
|
17
|
-
* override takes full precedence and bypasses this helper entirely.
|
|
18
|
-
*/
|
|
19
|
-
function scopedCacheDir(baseDir) {
|
|
20
|
-
const profile = getActiveProfile();
|
|
21
|
-
if (profile === undefined)
|
|
22
|
-
return baseDir;
|
|
23
|
-
const hash = createHash('sha256').update(profile).digest('hex').slice(0, 8);
|
|
24
|
-
const dir = path.join(baseDir, 'cache', hash);
|
|
25
|
-
if (!fs.existsSync(dir))
|
|
26
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
-
return dir;
|
|
28
|
-
}
|
|
29
|
-
/** GC cutoff for status entries: evict anything older than this. */
|
|
30
|
-
const DEFAULT_STATUS_GC_TTL_MS = 24 * 60 * 60 * 1000; // 24 h
|
|
31
|
-
function cacheFilePath() {
|
|
32
|
-
const override = getConfigPath();
|
|
33
|
-
const dir = override
|
|
34
|
-
? path.dirname(path.resolve(override))
|
|
35
|
-
: scopedCacheDir(path.join(os.homedir(), '.switchbot'));
|
|
36
|
-
return path.join(dir, 'devices.json');
|
|
37
|
-
}
|
|
38
|
-
// In-memory hot-cache keyed by active profile (or '__default__' for no profile).
|
|
39
|
-
// Using Maps instead of module-level singletons ensures that mcp serve, which
|
|
40
|
-
// rotates profiles per request via withRequestContext, never leaks inventory
|
|
41
|
-
// across profiles within the same process (Bug #37).
|
|
42
|
-
const _listCacheByProfile = new Map();
|
|
43
|
-
const _statusCacheByProfile = new Map();
|
|
44
|
-
function cacheKey() {
|
|
45
|
-
return getActiveProfile() ?? '__default__';
|
|
46
|
-
}
|
|
47
|
-
/** Force the next loadCache() call to re-read from disk. Used in tests. */
|
|
48
|
-
export function resetListCache() {
|
|
49
|
-
_listCacheByProfile.clear();
|
|
50
|
-
}
|
|
51
|
-
/** Force the next loadStatusCache() call to re-read from disk. Used in tests. */
|
|
52
|
-
export function resetStatusCache() {
|
|
53
|
-
_statusCacheByProfile.clear();
|
|
54
|
-
}
|
|
55
|
-
export function loadCache() {
|
|
56
|
-
const key = cacheKey();
|
|
57
|
-
if (_listCacheByProfile.has(key))
|
|
58
|
-
return _listCacheByProfile.get(key);
|
|
59
|
-
const file = cacheFilePath();
|
|
60
|
-
if (!fs.existsSync(file)) {
|
|
61
|
-
_listCacheByProfile.set(key, null);
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
66
|
-
const cache = JSON.parse(raw);
|
|
67
|
-
if (!cache || typeof cache.devices !== 'object' || cache.devices === null) {
|
|
68
|
-
_listCacheByProfile.set(key, null);
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
_listCacheByProfile.set(key, cache);
|
|
72
|
-
return cache;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
_listCacheByProfile.set(key, null);
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
export function getCachedDevice(deviceId) {
|
|
80
|
-
const cache = loadCache();
|
|
81
|
-
if (!cache)
|
|
82
|
-
return null;
|
|
83
|
-
return cache.devices[deviceId] ?? null;
|
|
84
|
-
}
|
|
85
|
-
/** Build a deviceId -> type map from the metadata cache. */
|
|
86
|
-
export function getCachedTypeMap(deviceIds) {
|
|
87
|
-
const cache = loadCache();
|
|
88
|
-
const out = new Map();
|
|
89
|
-
if (!cache)
|
|
90
|
-
return out;
|
|
91
|
-
if (deviceIds) {
|
|
92
|
-
for (const id of deviceIds) {
|
|
93
|
-
const entry = cache.devices[id];
|
|
94
|
-
if (entry?.type)
|
|
95
|
-
out.set(id, entry.type);
|
|
96
|
-
}
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
for (const [deviceId, entry] of Object.entries(cache.devices)) {
|
|
100
|
-
if (entry.type)
|
|
101
|
-
out.set(deviceId, entry.type);
|
|
102
|
-
}
|
|
103
|
-
return out;
|
|
104
|
-
}
|
|
105
|
-
export function updateCacheFromDeviceList(body) {
|
|
106
|
-
const devices = {};
|
|
107
|
-
for (const d of body.deviceList) {
|
|
108
|
-
if (!d.deviceId || !d.deviceType)
|
|
109
|
-
continue;
|
|
110
|
-
devices[d.deviceId] = {
|
|
111
|
-
type: d.deviceType,
|
|
112
|
-
name: d.deviceName,
|
|
113
|
-
category: 'physical',
|
|
114
|
-
hubDeviceId: d.hubDeviceId,
|
|
115
|
-
enableCloudService: d.enableCloudService,
|
|
116
|
-
roomID: d.roomID,
|
|
117
|
-
roomName: d.roomName,
|
|
118
|
-
familyName: d.familyName,
|
|
119
|
-
controlType: d.controlType,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
for (const d of body.infraredRemoteList) {
|
|
123
|
-
if (!d.deviceId)
|
|
124
|
-
continue;
|
|
125
|
-
devices[d.deviceId] = {
|
|
126
|
-
type: d.remoteType,
|
|
127
|
-
name: d.deviceName,
|
|
128
|
-
category: 'ir',
|
|
129
|
-
hubDeviceId: d.hubDeviceId,
|
|
130
|
-
controlType: d.controlType,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
const cache = {
|
|
134
|
-
lastUpdated: new Date().toISOString(),
|
|
135
|
-
devices,
|
|
136
|
-
};
|
|
137
|
-
try {
|
|
138
|
-
const file = cacheFilePath();
|
|
139
|
-
const dir = path.dirname(file);
|
|
140
|
-
if (!fs.existsSync(dir))
|
|
141
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
142
|
-
fs.writeFileSync(file, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
143
|
-
_listCacheByProfile.set(cacheKey(), cache);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Cache write failures must not break the command that triggered them.
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
export function clearCache() {
|
|
150
|
-
const file = cacheFilePath();
|
|
151
|
-
if (fs.existsSync(file))
|
|
152
|
-
fs.unlinkSync(file);
|
|
153
|
-
_listCacheByProfile.set(cacheKey(), null);
|
|
154
|
-
}
|
|
155
|
-
// ---- Device list freshness -------------------------------------------------
|
|
156
|
-
/** Age of the on-disk list cache in ms, or null if there is no cache. */
|
|
157
|
-
export function listCacheAgeMs(now = Date.now()) {
|
|
158
|
-
const cache = loadCache();
|
|
159
|
-
if (!cache)
|
|
160
|
-
return null;
|
|
161
|
-
const ts = Date.parse(cache.lastUpdated);
|
|
162
|
-
if (!Number.isFinite(ts))
|
|
163
|
-
return null;
|
|
164
|
-
return Math.max(0, now - ts);
|
|
165
|
-
}
|
|
166
|
-
/** True when the on-disk list cache is present and younger than `ttlMs`. */
|
|
167
|
-
export function isListCacheFresh(ttlMs, now = Date.now()) {
|
|
168
|
-
if (!ttlMs || ttlMs <= 0)
|
|
169
|
-
return false;
|
|
170
|
-
const age = listCacheAgeMs(now);
|
|
171
|
-
return age !== null && age < ttlMs;
|
|
172
|
-
}
|
|
173
|
-
function statusCacheFilePath() {
|
|
174
|
-
const override = getConfigPath();
|
|
175
|
-
const dir = override
|
|
176
|
-
? path.dirname(path.resolve(override))
|
|
177
|
-
: scopedCacheDir(path.join(os.homedir(), '.switchbot'));
|
|
178
|
-
return path.join(dir, 'status.json');
|
|
179
|
-
}
|
|
180
|
-
export function loadStatusCache() {
|
|
181
|
-
const key = cacheKey();
|
|
182
|
-
if (_statusCacheByProfile.has(key))
|
|
183
|
-
return _statusCacheByProfile.get(key);
|
|
184
|
-
const file = statusCacheFilePath();
|
|
185
|
-
if (!fs.existsSync(file)) {
|
|
186
|
-
const empty = { entries: {} };
|
|
187
|
-
_statusCacheByProfile.set(key, empty);
|
|
188
|
-
return empty;
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
192
|
-
const parsed = JSON.parse(raw);
|
|
193
|
-
if (!parsed || typeof parsed.entries !== 'object' || parsed.entries === null) {
|
|
194
|
-
const empty = { entries: {} };
|
|
195
|
-
_statusCacheByProfile.set(key, empty);
|
|
196
|
-
return empty;
|
|
197
|
-
}
|
|
198
|
-
_statusCacheByProfile.set(key, parsed);
|
|
199
|
-
return parsed;
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
const empty = { entries: {} };
|
|
203
|
-
_statusCacheByProfile.set(key, empty);
|
|
204
|
-
return empty;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
function saveStatusCache(cache) {
|
|
208
|
-
_statusCacheByProfile.set(cacheKey(), cache);
|
|
209
|
-
try {
|
|
210
|
-
const file = statusCacheFilePath();
|
|
211
|
-
const dir = path.dirname(file);
|
|
212
|
-
if (!fs.existsSync(dir))
|
|
213
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
214
|
-
fs.writeFileSync(file, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
/* best-effort */
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
/** Read a status entry; returns null when missing or older than `ttlMs`. */
|
|
221
|
-
export function getCachedStatus(deviceId, ttlMs, now = Date.now()) {
|
|
222
|
-
if (!ttlMs || ttlMs <= 0)
|
|
223
|
-
return null;
|
|
224
|
-
const cache = loadStatusCache();
|
|
225
|
-
const entry = cache.entries[deviceId];
|
|
226
|
-
if (!entry)
|
|
227
|
-
return null;
|
|
228
|
-
const ts = Date.parse(entry.fetchedAt);
|
|
229
|
-
if (!Number.isFinite(ts))
|
|
230
|
-
return null;
|
|
231
|
-
if (now - ts >= ttlMs)
|
|
232
|
-
return null;
|
|
233
|
-
return entry.body;
|
|
234
|
-
}
|
|
235
|
-
/** Evict status entries older than max(ttlMs × 10, 24 h) to bound file growth. */
|
|
236
|
-
function evictExpiredStatusEntries(cache, ttlMs, now = Date.now()) {
|
|
237
|
-
const cutoff = now - Math.max(ttlMs * 10, 24 * 60 * 60 * 1000);
|
|
238
|
-
for (const id of Object.keys(cache.entries)) {
|
|
239
|
-
const entry = cache.entries[id];
|
|
240
|
-
const ts = Date.parse(entry.fetchedAt);
|
|
241
|
-
if (!Number.isFinite(ts) || ts < cutoff)
|
|
242
|
-
delete cache.entries[id];
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
export function setCachedStatus(deviceId, body, now = new Date(), ttlMsForGc = DEFAULT_STATUS_GC_TTL_MS) {
|
|
246
|
-
const cache = loadStatusCache();
|
|
247
|
-
cache.entries[deviceId] = {
|
|
248
|
-
fetchedAt: now.toISOString(),
|
|
249
|
-
body,
|
|
250
|
-
};
|
|
251
|
-
evictExpiredStatusEntries(cache, ttlMsForGc, now.getTime());
|
|
252
|
-
saveStatusCache(cache);
|
|
253
|
-
}
|
|
254
|
-
export function clearStatusCache() {
|
|
255
|
-
const file = statusCacheFilePath();
|
|
256
|
-
if (fs.existsSync(file))
|
|
257
|
-
fs.unlinkSync(file);
|
|
258
|
-
_statusCacheByProfile.set(cacheKey(), { entries: {} });
|
|
259
|
-
}
|
|
260
|
-
export function describeCache(now = Date.now()) {
|
|
261
|
-
const listFile = cacheFilePath();
|
|
262
|
-
const listCache = loadCache();
|
|
263
|
-
const listExists = fs.existsSync(listFile);
|
|
264
|
-
const list = {
|
|
265
|
-
path: listFile,
|
|
266
|
-
exists: listExists,
|
|
267
|
-
};
|
|
268
|
-
if (listCache) {
|
|
269
|
-
list.lastUpdated = listCache.lastUpdated;
|
|
270
|
-
const ts = Date.parse(listCache.lastUpdated);
|
|
271
|
-
if (Number.isFinite(ts))
|
|
272
|
-
list.ageMs = Math.max(0, now - ts);
|
|
273
|
-
list.deviceCount = Object.keys(listCache.devices).length;
|
|
274
|
-
}
|
|
275
|
-
const statusFile = statusCacheFilePath();
|
|
276
|
-
const statusExists = fs.existsSync(statusFile);
|
|
277
|
-
const statusCache = loadStatusCache();
|
|
278
|
-
const entries = Object.values(statusCache.entries);
|
|
279
|
-
const status = {
|
|
280
|
-
path: statusFile,
|
|
281
|
-
exists: statusExists,
|
|
282
|
-
entryCount: entries.length,
|
|
283
|
-
};
|
|
284
|
-
if (entries.length > 0) {
|
|
285
|
-
const sorted = entries
|
|
286
|
-
.map((e) => e.fetchedAt)
|
|
287
|
-
.filter((s) => typeof s === 'string')
|
|
288
|
-
.sort();
|
|
289
|
-
status.oldestFetchedAt = sorted[0];
|
|
290
|
-
status.newestFetchedAt = sorted[sorted.length - 1];
|
|
291
|
-
}
|
|
292
|
-
return { list, status };
|
|
293
|
-
}
|