@switchbot/openapi-cli 3.1.0 → 3.1.1
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/commands/daemon.js +48 -5
- package/dist/commands/upgrade-check.js +19 -0
- package/dist/rules/action.js +11 -0
- package/dist/rules/conflict-analyzer.js +17 -6
- package/dist/rules/suggest.js +1 -1
- package/dist/utils/audit.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ Under the hood every surface shares the same catalog, cache, and HMAC client —
|
|
|
73
73
|
- [`capabilities`](#capabilities--cli-manifest)
|
|
74
74
|
- [`cache`](#cache--inspect-and-clear-local-cache)
|
|
75
75
|
- [`policy`](#policy--validate-scaffold-and-migrate-policyyaml)
|
|
76
|
+
- [`daemon`](#daemon--background-rules-engine-process)
|
|
76
77
|
- [`completion`](#completion--shell-tab-completion)
|
|
77
78
|
- [Output modes](#output-modes)
|
|
78
79
|
- [Cache](#cache)
|
|
@@ -80,8 +81,6 @@ Under the hood every surface shares the same catalog, cache, and HMAC client —
|
|
|
80
81
|
- [Environment variables](#environment-variables)
|
|
81
82
|
- [Scripting examples](#scripting-examples)
|
|
82
83
|
- [Development](#development)
|
|
83
|
-
- [Contributing](#contributing)
|
|
84
|
-
- [Roadmap](#roadmap)
|
|
85
84
|
- [License](#license)
|
|
86
85
|
- [References](#references)
|
|
87
86
|
|
|
@@ -94,7 +93,7 @@ Under the hood every surface shares the same catalog, cache, and HMAC client —
|
|
|
94
93
|
- 🎨 **Dual output modes** — colorized tables by default; `--json` passthrough for `jq` and scripting
|
|
95
94
|
- 🔐 **Secure credentials** — HMAC-SHA256 signed requests; config file written with `0600`; env-var override for CI
|
|
96
95
|
- 🔍 **Dry-run mode** — preview every mutating request before it hits the API
|
|
97
|
-
- 🧪 **Fully tested** —
|
|
96
|
+
- 🧪 **Fully tested** — 1882 Vitest tests, mocked axios, zero network in CI
|
|
98
97
|
- ⚡ **Shell completion** — Bash / Zsh / Fish / PowerShell
|
|
99
98
|
|
|
100
99
|
## Requirements
|
|
@@ -743,6 +742,34 @@ switchbot events mqtt-tail --sink homeassistant --ha-url http://homeassistant.lo
|
|
|
743
742
|
|
|
744
743
|
Device state is also persisted to `~/.switchbot/device-history/<deviceId>.json` (latest + 100-entry ring buffer) regardless of sink configuration. This enables the `get_device_history` MCP tool to answer state queries without an API call.
|
|
745
744
|
|
|
745
|
+
### `daemon` — background rules-engine process
|
|
746
|
+
|
|
747
|
+
Runs `switchbot rules run` as a detached background process. Tracks runtime
|
|
748
|
+
metadata in `~/.switchbot/daemon.state.json` and can co-launch a health HTTP
|
|
749
|
+
server.
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# Start the daemon (no-op if already running)
|
|
753
|
+
switchbot daemon start
|
|
754
|
+
switchbot daemon start --policy ./my-policy.yaml
|
|
755
|
+
switchbot daemon start --healthz-port 3100 # also launch health serve on port 3100
|
|
756
|
+
switchbot daemon start --force # restart even if already running
|
|
757
|
+
|
|
758
|
+
# Inspect daemon state (pid, log path, health server, last reload)
|
|
759
|
+
switchbot daemon status
|
|
760
|
+
switchbot daemon status --json
|
|
761
|
+
|
|
762
|
+
# Hot-reload policy without restarting (sends SIGHUP on Unix, writes sentinel on Windows)
|
|
763
|
+
switchbot daemon reload
|
|
764
|
+
|
|
765
|
+
# Stop the daemon and any co-launched health server
|
|
766
|
+
switchbot daemon stop
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
Start prints the PID, log path, and state file location. If the process exits
|
|
770
|
+
within 300 ms of launch, start fails immediately and includes the last 20 lines
|
|
771
|
+
of the log in the error message for fast diagnosis.
|
|
772
|
+
|
|
746
773
|
### `completion` — shell tab-completion
|
|
747
774
|
|
|
748
775
|
```bash
|
|
@@ -862,7 +889,7 @@ switchbot upgrade-check --json # structured JSON output
|
|
|
862
889
|
switchbot upgrade-check --timeout 5000 # custom registry timeout (ms)
|
|
863
890
|
```
|
|
864
891
|
|
|
865
|
-
Queries the npm registry for the latest published version and compares it against the running version.
|
|
892
|
+
Queries the npm registry for the latest published version and compares it against the running version. When the registry's `dist-tags.latest` is itself a prerelease (e.g. `4.0.0-rc.1`), the check is skipped and the current version is treated as up-to-date — accidental prerelease tags don't trigger spurious upgrade prompts.
|
|
866
893
|
`--json` output:
|
|
867
894
|
|
|
868
895
|
```json
|
|
@@ -1096,7 +1123,7 @@ npm install
|
|
|
1096
1123
|
|
|
1097
1124
|
npm run dev -- <args> # Run from TypeScript sources via tsx
|
|
1098
1125
|
npm run build # Compile to dist/
|
|
1099
|
-
npm test # Run the Vitest suite (
|
|
1126
|
+
npm test # Run the Vitest suite (1882 tests)
|
|
1100
1127
|
npm run test:watch # Watch mode
|
|
1101
1128
|
npm run test:coverage # Coverage report (v8, HTML + text)
|
|
1102
1129
|
```
|
|
@@ -1152,7 +1179,7 @@ src/
|
|
|
1152
1179
|
│ ├── explain.ts # `devices explain` — one-shot device summary
|
|
1153
1180
|
│ ├── device-meta.ts # `devices meta` — local aliases / hide flags
|
|
1154
1181
|
│ ├── install.ts # `switchbot install` / `uninstall`
|
|
1155
|
-
│ ├── policy.ts # `policy validate/new/migrate/diff/add-rule`
|
|
1182
|
+
│ ├── policy.ts # `policy validate/new/migrate/diff/add-rule/backup/restore`
|
|
1156
1183
|
│ ├── rules.ts # `rules suggest/lint/list/explain/run/reload/tail/replay/
|
|
1157
1184
|
│ │ # conflicts/doctor/summary/last-fired/webhook-*`
|
|
1158
1185
|
│ ├── scenes.ts
|
|
@@ -1178,7 +1205,7 @@ src/
|
|
|
1178
1205
|
├── format.ts # renderRows / filterFields / output-format dispatch
|
|
1179
1206
|
├── audit.ts # JSONL audit log writer
|
|
1180
1207
|
└── quota.ts # Local daily-quota counter
|
|
1181
|
-
tests/ # Vitest suite (
|
|
1208
|
+
tests/ # Vitest suite (1882 tests, mocked axios, no network)
|
|
1182
1209
|
```
|
|
1183
1210
|
|
|
1184
1211
|
### Release flow
|
|
@@ -1192,41 +1219,6 @@ git push --follow-tags
|
|
|
1192
1219
|
|
|
1193
1220
|
Then on GitHub → **Releases → Draft a new release → select tag → Publish**. The `publish.yml` workflow runs tests, verifies the tag matches `package.json`, and publishes `@switchbot/openapi-cli` to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements).
|
|
1194
1221
|
|
|
1195
|
-
## Contributing
|
|
1196
|
-
|
|
1197
|
-
Bug reports, feature requests, and PRs are welcome.
|
|
1198
|
-
|
|
1199
|
-
1. Fork the repo and create a topic branch.
|
|
1200
|
-
2. Keep changes small and focused; add or update Vitest cases for any behavior change.
|
|
1201
|
-
3. Run `npm test` and `npm run build` locally — both must pass.
|
|
1202
|
-
4. Open a pull request against `main`. CI runs on Node 18/20/22; all three must stay green.
|
|
1203
|
-
|
|
1204
|
-
## Roadmap
|
|
1205
|
-
|
|
1206
|
-
Phase 1 through Phase 4 are shipped. The authoritative phase/track table
|
|
1207
|
-
(including skill-side `autonomyLevel` L1/L2/L3 mapping) lives in
|
|
1208
|
-
[`docs/design/roadmap.md`](./docs/design/roadmap.md).
|
|
1209
|
-
|
|
1210
|
-
Shipped tracks summary:
|
|
1211
|
-
|
|
1212
|
-
- **Track β**: one-command install/uninstall surface (`switchbot install` / `switchbot uninstall`).
|
|
1213
|
-
- **Track γ**: rules v0.2 runtime increment (`days` + `all`/`any`/`not`).
|
|
1214
|
-
- **Track δ (L2)**: plan authoring + guarded execution (`plan suggest`, `plan run --require-approval`) and MCP review/execute tools (`plan_suggest`, `plan_run`, `audit_query`, `audit_stats`, `policy_diff`).
|
|
1215
|
-
- **Track ζ (L3)**: autonomous rule authoring (`rules suggest`, `policy add-rule`) with MCP parity (`rules_suggest`, `policy_add_rule`).
|
|
1216
|
-
- **Track ε**: cross-OS keychain CI matrix (macOS + Linux libsecret + Windows Credential Manager).
|
|
1217
|
-
|
|
1218
|
-
Backlog tracks still open:
|
|
1219
|
-
|
|
1220
|
-
1. **Daemon mode** — long-running local process with Unix/named-pipe
|
|
1221
|
-
transport so repeated MCP or plan invocations avoid fresh-process
|
|
1222
|
-
startup cost.
|
|
1223
|
-
2. **`npx @switchbot/mcp-server`** — split the MCP server into a tiny
|
|
1224
|
-
package so non-CLI users can run it directly with `npx`.
|
|
1225
|
-
3. **`switchbot self-test`** — scripted end-to-end go/no-go checks for
|
|
1226
|
-
token/secret validity plus representative device control.
|
|
1227
|
-
4. **Record / replay** — capture request/response fixtures and replay
|
|
1228
|
-
offline for deterministic integration tests and CI.
|
|
1229
|
-
|
|
1230
1222
|
## License
|
|
1231
1223
|
|
|
1232
1224
|
[MIT](./LICENSE) © chenliuyun
|
package/dist/commands/daemon.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { isJsonMode, printJson, exitWithError } from '../utils/output.js';
|
|
6
|
-
import { readPidFile, writePidFile, isPidAlive, getDefaultPidFilePaths, writeReloadSentinel, sighupSupported } from '../rules/pid-file.js';
|
|
6
|
+
import { readPidFile, writePidFile, clearPidFile, isPidAlive, getDefaultPidFilePaths, writeReloadSentinel, sighupSupported } from '../rules/pid-file.js';
|
|
7
7
|
import { stringArg } from '../utils/arg-parsers.js';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { DAEMON_LOG_FILE, DAEMON_PID_FILE, DAEMON_STATE_FILE, HEALTHZ_PID_FILE, readDaemonState, writeDaemonState, } from '../lib/daemon-state.js';
|
|
@@ -65,6 +65,39 @@ function persistState(partial) {
|
|
|
65
65
|
writeDaemonState(next);
|
|
66
66
|
return next;
|
|
67
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
|
+
}
|
|
68
101
|
function renderHumanStatus(status) {
|
|
69
102
|
if (status.status === 'running' && status.pid !== null) {
|
|
70
103
|
console.log(`${chalk.green('●')} Daemon is running (pid ${status.pid}).`);
|
|
@@ -118,7 +151,7 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
118
151
|
.option('--policy <path>', 'Policy file path (default: auto-detected)', stringArg('--policy'))
|
|
119
152
|
.option('--force', 'Restart even if the daemon appears to be running.')
|
|
120
153
|
.option('--healthz-port <n>', 'Also start a health HTTP server on this port (default: disabled).')
|
|
121
|
-
.action((opts) => {
|
|
154
|
+
.action(async (opts) => {
|
|
122
155
|
const current = getDaemonStatus();
|
|
123
156
|
if (current.status === 'running' && !opts.force) {
|
|
124
157
|
if (isJsonMode()) {
|
|
@@ -139,7 +172,7 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
174
|
const thisFile = fileURLToPath(import.meta.url);
|
|
142
|
-
const cliEntry = path.resolve(path.dirname(thisFile), 'index.js');
|
|
175
|
+
const cliEntry = path.resolve(path.dirname(thisFile), '..', 'index.js');
|
|
143
176
|
const args = ['rules', 'run'];
|
|
144
177
|
if (opts.policy)
|
|
145
178
|
args.push(opts.policy);
|
|
@@ -163,6 +196,11 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
163
196
|
});
|
|
164
197
|
child.unref();
|
|
165
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
|
+
});
|
|
166
204
|
const newPid = child.pid;
|
|
167
205
|
if (!newPid) {
|
|
168
206
|
persistState({
|
|
@@ -187,8 +225,13 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
187
225
|
healthChild.unref();
|
|
188
226
|
fs.closeSync(healthLogFd);
|
|
189
227
|
if (healthChild.pid) {
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
192
235
|
}
|
|
193
236
|
}
|
|
194
237
|
persistState({
|
|
@@ -6,6 +6,8 @@ const require = createRequire(import.meta.url);
|
|
|
6
6
|
const { name: pkgName, version: currentVersion } = require('../../package.json');
|
|
7
7
|
function fetchLatestVersion(packageName, timeoutMs = 8000) {
|
|
8
8
|
const encoded = packageName.replace('/', '%2F');
|
|
9
|
+
// /latest is shorthand for dist-tags.latest — always the current stable
|
|
10
|
+
// release tag, never a prerelease unless accidentally published as such.
|
|
9
11
|
const url = `https://registry.npmjs.org/${encoded}/latest`;
|
|
10
12
|
return new Promise((resolve, reject) => {
|
|
11
13
|
const req = https.get(url, { timeout: timeoutMs }, (res) => {
|
|
@@ -28,6 +30,9 @@ function fetchLatestVersion(packageName, timeoutMs = 8000) {
|
|
|
28
30
|
req.on('error', reject);
|
|
29
31
|
});
|
|
30
32
|
}
|
|
33
|
+
// Intentionally avoids the `semver` npm package (YAGNI): comparing two
|
|
34
|
+
// well-formed registry version strings needs only these 10 lines, and adding
|
|
35
|
+
// a runtime dep solely for version comparison would bloat install footprint.
|
|
31
36
|
function semverGt(a, b) {
|
|
32
37
|
const numParts = (v) => v.replace(/-.*$/, '').split('.').map((n) => Number.parseInt(n, 10));
|
|
33
38
|
const [aMaj, aMin, aPat] = numParts(a);
|
|
@@ -64,6 +69,20 @@ export function registerUpgradeCheckCommand(program) {
|
|
|
64
69
|
const upToDate = !semverGt(latestVersion, currentVersion);
|
|
65
70
|
const currentMajor = Number.parseInt(currentVersion.split('.')[0], 10);
|
|
66
71
|
const latestMajor = Number.parseInt(latestVersion.split('.')[0], 10);
|
|
72
|
+
if (latestVersion.includes('-')) {
|
|
73
|
+
const msg = `Latest registry version (${latestVersion}) is a prerelease — skipping update check.`;
|
|
74
|
+
if (isJsonMode()) {
|
|
75
|
+
printJson({
|
|
76
|
+
current: currentVersion, latest: latestVersion, upToDate: true,
|
|
77
|
+
updateAvailable: false, breakingChange: false, installCommand: null,
|
|
78
|
+
note: msg,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`${chalk.green('✓')} You are running the latest stable version (${currentVersion}). Registry latest (${latestVersion}) is a prerelease — skipping.`);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
67
86
|
const result = {
|
|
68
87
|
current: currentVersion,
|
|
69
88
|
latest: latestVersion,
|
package/dist/rules/action.js
CHANGED
|
@@ -203,3 +203,14 @@ export async function executeRuleAction(action, ctx) {
|
|
|
203
203
|
return { ok: false, error: msg, deviceId, verb: parsed.verb };
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Extract the raw deviceId from an action object without alias resolution.
|
|
208
|
+
* Prefers `action.device` over the deviceId embedded in the command string.
|
|
209
|
+
* Use resolveActionDevice() when alias resolution is needed.
|
|
210
|
+
*/
|
|
211
|
+
export function extractDeviceIdFromAction(action) {
|
|
212
|
+
if (action.device)
|
|
213
|
+
return action.device;
|
|
214
|
+
const m = /\bdevices\s+command\s+(\S+)/.exec(action.command ?? '');
|
|
215
|
+
return m ? m[1] : null;
|
|
216
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { isTimeBetween, isAllCondition, isAnyCondition, isNotCondition } from './types.js';
|
|
26
26
|
import { parseMaxPerMs } from './throttle.js';
|
|
27
27
|
import { isDestructiveCommand } from './destructive.js';
|
|
28
|
+
import { extractDeviceIdFromAction } from './action.js';
|
|
28
29
|
/** Known opposing command pairs (order-independent). */
|
|
29
30
|
const OPPOSING_PAIRS = [
|
|
30
31
|
['turnOn', 'turnOff'],
|
|
@@ -37,6 +38,19 @@ const OPPOSING_PAIRS = [
|
|
|
37
38
|
['volumeUp', 'volumeDown'],
|
|
38
39
|
['fanSpeedUp', 'fanSpeedDown'],
|
|
39
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* MQTT events that fire on every device state push and can rapidly exhaust
|
|
43
|
+
* the daily API quota when a rule has no throttle.
|
|
44
|
+
*
|
|
45
|
+
* Conditional events like `motion.detected` are intentionally excluded —
|
|
46
|
+
* they fire discretely, not at continuous high frequency. Extend this set
|
|
47
|
+
* when new catch-all or near-continuous event types are added to the classifier.
|
|
48
|
+
*/
|
|
49
|
+
export const HIGH_FREQ_EVENTS = ['device.shadow', '*'];
|
|
50
|
+
/** Returns true when an MQTT event is known to fire at high frequency. */
|
|
51
|
+
export function isHighFreqEvent(event) {
|
|
52
|
+
return HIGH_FREQ_EVENTS.includes(event);
|
|
53
|
+
}
|
|
40
54
|
function commandsAreOpposing(a, b) {
|
|
41
55
|
for (const [x, y] of OPPOSING_PAIRS) {
|
|
42
56
|
if ((a === x && b === y) || (a === y && b === x))
|
|
@@ -44,9 +58,6 @@ function commandsAreOpposing(a, b) {
|
|
|
44
58
|
}
|
|
45
59
|
return false;
|
|
46
60
|
}
|
|
47
|
-
function extractDeviceFromAction(action) {
|
|
48
|
-
return action.device ?? null;
|
|
49
|
-
}
|
|
50
61
|
function extractCommandVerb(command) {
|
|
51
62
|
// command strings are like "devices command <id> turnOn" — extract last token
|
|
52
63
|
const parts = command.trim().split(/\s+/);
|
|
@@ -108,8 +119,8 @@ export function analyzeConflicts(rules, quietHours) {
|
|
|
108
119
|
continue;
|
|
109
120
|
for (const actionA of a.then) {
|
|
110
121
|
for (const actionB of b.then) {
|
|
111
|
-
const deviceA =
|
|
112
|
-
const deviceB =
|
|
122
|
+
const deviceA = extractDeviceIdFromAction(actionA);
|
|
123
|
+
const deviceB = extractDeviceIdFromAction(actionB);
|
|
113
124
|
// Skip if devices can't be compared.
|
|
114
125
|
if (!deviceA || !deviceB || deviceA !== deviceB)
|
|
115
126
|
continue;
|
|
@@ -136,7 +147,7 @@ export function analyzeConflicts(rules, quietHours) {
|
|
|
136
147
|
if (rule.when.source !== 'mqtt')
|
|
137
148
|
continue;
|
|
138
149
|
const event = rule.when.event;
|
|
139
|
-
const isHighFreq = event
|
|
150
|
+
const isHighFreq = isHighFreqEvent(event);
|
|
140
151
|
if (!isHighFreq)
|
|
141
152
|
continue;
|
|
142
153
|
const cooldown = effectiveCooldownMs(rule);
|
package/dist/rules/suggest.js
CHANGED
|
@@ -80,7 +80,7 @@ export function suggestRule(opts) {
|
|
|
80
80
|
const then = actionDevices.length > 0
|
|
81
81
|
? actionDevices.map((d) => ({
|
|
82
82
|
command: `devices command <id> ${command}`,
|
|
83
|
-
device: d.
|
|
83
|
+
device: d.id,
|
|
84
84
|
}))
|
|
85
85
|
: [{ command: `devices command <id> ${command}` }];
|
|
86
86
|
const rule = {
|
package/dist/utils/audit.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getAuditLog } from './flags.js';
|
|
5
|
+
export const DEFAULT_AUDIT_PATH = path.join(os.homedir(), '.switchbot', 'audit.log');
|
|
4
6
|
/**
|
|
5
7
|
* Bump when breaking changes to the audit line shape land.
|
|
6
8
|
*
|
|
@@ -20,7 +22,9 @@ function resolveAuditPath() {
|
|
|
20
22
|
return path.resolve(flag);
|
|
21
23
|
}
|
|
22
24
|
export function writeAudit(entry) {
|
|
23
|
-
|
|
25
|
+
let file = resolveAuditPath();
|
|
26
|
+
if (!file && entry.planId)
|
|
27
|
+
file = DEFAULT_AUDIT_PATH;
|
|
24
28
|
if (!file)
|
|
25
29
|
return;
|
|
26
30
|
const dir = path.dirname(file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchbot/openapi-cli",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"switchbot",
|