@switchbot/openapi-cli 3.2.1 → 3.3.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 (332) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +57419 -170
  3. package/package.json +9 -5
  4. package/dist/api/client.d.ts +0 -31
  5. package/dist/api/client.js +0 -236
  6. package/dist/api/client.js.map +0 -1
  7. package/dist/auth.d.ts +0 -1
  8. package/dist/auth.js +0 -21
  9. package/dist/auth.js.map +0 -1
  10. package/dist/commands/agent-bootstrap.d.ts +0 -10
  11. package/dist/commands/agent-bootstrap.js +0 -200
  12. package/dist/commands/agent-bootstrap.js.map +0 -1
  13. package/dist/commands/auth.d.ts +0 -18
  14. package/dist/commands/auth.js +0 -355
  15. package/dist/commands/auth.js.map +0 -1
  16. package/dist/commands/batch.d.ts +0 -2
  17. package/dist/commands/batch.js +0 -414
  18. package/dist/commands/batch.js.map +0 -1
  19. package/dist/commands/cache.d.ts +0 -2
  20. package/dist/commands/cache.js +0 -127
  21. package/dist/commands/cache.js.map +0 -1
  22. package/dist/commands/capabilities.d.ts +0 -31
  23. package/dist/commands/capabilities.js +0 -383
  24. package/dist/commands/capabilities.js.map +0 -1
  25. package/dist/commands/catalog.d.ts +0 -2
  26. package/dist/commands/catalog.js +0 -360
  27. package/dist/commands/catalog.js.map +0 -1
  28. package/dist/commands/completion.d.ts +0 -2
  29. package/dist/commands/completion.js +0 -386
  30. package/dist/commands/completion.js.map +0 -1
  31. package/dist/commands/config.d.ts +0 -21
  32. package/dist/commands/config.js +0 -377
  33. package/dist/commands/config.js.map +0 -1
  34. package/dist/commands/daemon.d.ts +0 -2
  35. package/dist/commands/daemon.js +0 -411
  36. package/dist/commands/daemon.js.map +0 -1
  37. package/dist/commands/device-meta.d.ts +0 -2
  38. package/dist/commands/device-meta.js +0 -160
  39. package/dist/commands/device-meta.js.map +0 -1
  40. package/dist/commands/devices.d.ts +0 -2
  41. package/dist/commands/devices.js +0 -949
  42. package/dist/commands/devices.js.map +0 -1
  43. package/dist/commands/doctor.d.ts +0 -3
  44. package/dist/commands/doctor.js +0 -1016
  45. package/dist/commands/doctor.js.map +0 -1
  46. package/dist/commands/events.d.ts +0 -31
  47. package/dist/commands/events.js +0 -564
  48. package/dist/commands/events.js.map +0 -1
  49. package/dist/commands/expand.d.ts +0 -2
  50. package/dist/commands/expand.js +0 -131
  51. package/dist/commands/expand.js.map +0 -1
  52. package/dist/commands/explain.d.ts +0 -2
  53. package/dist/commands/explain.js +0 -140
  54. package/dist/commands/explain.js.map +0 -1
  55. package/dist/commands/health.d.ts +0 -8
  56. package/dist/commands/health.js +0 -114
  57. package/dist/commands/health.js.map +0 -1
  58. package/dist/commands/history.d.ts +0 -2
  59. package/dist/commands/history.js +0 -321
  60. package/dist/commands/history.js.map +0 -1
  61. package/dist/commands/identity.d.ts +0 -45
  62. package/dist/commands/identity.js +0 -60
  63. package/dist/commands/identity.js.map +0 -1
  64. package/dist/commands/install.d.ts +0 -20
  65. package/dist/commands/install.js +0 -247
  66. package/dist/commands/install.js.map +0 -1
  67. package/dist/commands/mcp.d.ts +0 -14
  68. package/dist/commands/mcp.js +0 -2018
  69. package/dist/commands/mcp.js.map +0 -1
  70. package/dist/commands/plan.d.ts +0 -51
  71. package/dist/commands/plan.js +0 -654
  72. package/dist/commands/plan.js.map +0 -1
  73. package/dist/commands/policy.d.ts +0 -24
  74. package/dist/commands/policy.js +0 -587
  75. package/dist/commands/policy.js.map +0 -1
  76. package/dist/commands/quota.d.ts +0 -2
  77. package/dist/commands/quota.js +0 -79
  78. package/dist/commands/quota.js.map +0 -1
  79. package/dist/commands/rules.d.ts +0 -2
  80. package/dist/commands/rules.js +0 -876
  81. package/dist/commands/rules.js.map +0 -1
  82. package/dist/commands/scenes.d.ts +0 -2
  83. package/dist/commands/scenes.js +0 -265
  84. package/dist/commands/scenes.js.map +0 -1
  85. package/dist/commands/schema.d.ts +0 -2
  86. package/dist/commands/schema.js +0 -185
  87. package/dist/commands/schema.js.map +0 -1
  88. package/dist/commands/status-sync.d.ts +0 -2
  89. package/dist/commands/status-sync.js +0 -132
  90. package/dist/commands/status-sync.js.map +0 -1
  91. package/dist/commands/uninstall.d.ts +0 -20
  92. package/dist/commands/uninstall.js +0 -238
  93. package/dist/commands/uninstall.js.map +0 -1
  94. package/dist/commands/upgrade-check.d.ts +0 -2
  95. package/dist/commands/upgrade-check.js +0 -107
  96. package/dist/commands/upgrade-check.js.map +0 -1
  97. package/dist/commands/watch.d.ts +0 -2
  98. package/dist/commands/watch.js +0 -195
  99. package/dist/commands/watch.js.map +0 -1
  100. package/dist/commands/webhook.d.ts +0 -2
  101. package/dist/commands/webhook.js +0 -183
  102. package/dist/commands/webhook.js.map +0 -1
  103. package/dist/config.d.ts +0 -57
  104. package/dist/config.js +0 -259
  105. package/dist/config.js.map +0 -1
  106. package/dist/credentials/backends/file.d.ts +0 -18
  107. package/dist/credentials/backends/file.js +0 -102
  108. package/dist/credentials/backends/file.js.map +0 -1
  109. package/dist/credentials/backends/linux.d.ts +0 -16
  110. package/dist/credentials/backends/linux.js +0 -130
  111. package/dist/credentials/backends/linux.js.map +0 -1
  112. package/dist/credentials/backends/macos.d.ts +0 -18
  113. package/dist/credentials/backends/macos.js +0 -130
  114. package/dist/credentials/backends/macos.js.map +0 -1
  115. package/dist/credentials/backends/windows.d.ts +0 -23
  116. package/dist/credentials/backends/windows.js +0 -216
  117. package/dist/credentials/backends/windows.js.map +0 -1
  118. package/dist/credentials/keychain.d.ts +0 -83
  119. package/dist/credentials/keychain.js +0 -89
  120. package/dist/credentials/keychain.js.map +0 -1
  121. package/dist/credentials/prime.d.ts +0 -32
  122. package/dist/credentials/prime.js +0 -53
  123. package/dist/credentials/prime.js.map +0 -1
  124. package/dist/devices/cache.d.ts +0 -79
  125. package/dist/devices/cache.js +0 -294
  126. package/dist/devices/cache.js.map +0 -1
  127. package/dist/devices/catalog.d.ts +0 -138
  128. package/dist/devices/catalog.js +0 -768
  129. package/dist/devices/catalog.js.map +0 -1
  130. package/dist/devices/device-meta.d.ts +0 -15
  131. package/dist/devices/device-meta.js +0 -57
  132. package/dist/devices/device-meta.js.map +0 -1
  133. package/dist/devices/history-agg.d.ts +0 -37
  134. package/dist/devices/history-agg.js +0 -139
  135. package/dist/devices/history-agg.js.map +0 -1
  136. package/dist/devices/history-query.d.ts +0 -45
  137. package/dist/devices/history-query.js +0 -182
  138. package/dist/devices/history-query.js.map +0 -1
  139. package/dist/devices/param-validator.d.ts +0 -40
  140. package/dist/devices/param-validator.js +0 -434
  141. package/dist/devices/param-validator.js.map +0 -1
  142. package/dist/devices/resources.d.ts +0 -74
  143. package/dist/devices/resources.js +0 -271
  144. package/dist/devices/resources.js.map +0 -1
  145. package/dist/index.d.ts +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/install/default-steps.d.ts +0 -66
  148. package/dist/install/default-steps.js +0 -258
  149. package/dist/install/default-steps.js.map +0 -1
  150. package/dist/install/preflight.d.ts +0 -60
  151. package/dist/install/preflight.js +0 -213
  152. package/dist/install/preflight.js.map +0 -1
  153. package/dist/install/steps.d.ts +0 -61
  154. package/dist/install/steps.js +0 -68
  155. package/dist/install/steps.js.map +0 -1
  156. package/dist/lib/command-keywords.d.ts +0 -5
  157. package/dist/lib/command-keywords.js +0 -18
  158. package/dist/lib/command-keywords.js.map +0 -1
  159. package/dist/lib/daemon-state.d.ts +0 -24
  160. package/dist/lib/daemon-state.js +0 -47
  161. package/dist/lib/daemon-state.js.map +0 -1
  162. package/dist/lib/destructive-mode.d.ts +0 -2
  163. package/dist/lib/destructive-mode.js +0 -13
  164. package/dist/lib/destructive-mode.js.map +0 -1
  165. package/dist/lib/devices.d.ts +0 -151
  166. package/dist/lib/devices.js +0 -383
  167. package/dist/lib/devices.js.map +0 -1
  168. package/dist/lib/idempotency.d.ts +0 -46
  169. package/dist/lib/idempotency.js +0 -107
  170. package/dist/lib/idempotency.js.map +0 -1
  171. package/dist/lib/plan-store.d.ts +0 -19
  172. package/dist/lib/plan-store.js +0 -69
  173. package/dist/lib/plan-store.js.map +0 -1
  174. package/dist/lib/request-context.d.ts +0 -7
  175. package/dist/lib/request-context.js +0 -13
  176. package/dist/lib/request-context.js.map +0 -1
  177. package/dist/lib/scenes.d.ts +0 -7
  178. package/dist/lib/scenes.js +0 -11
  179. package/dist/lib/scenes.js.map +0 -1
  180. package/dist/logger.d.ts +0 -4
  181. package/dist/logger.js +0 -17
  182. package/dist/logger.js.map +0 -1
  183. package/dist/mcp/device-history.d.ts +0 -36
  184. package/dist/mcp/device-history.js +0 -146
  185. package/dist/mcp/device-history.js.map +0 -1
  186. package/dist/mcp/events-subscription.d.ts +0 -45
  187. package/dist/mcp/events-subscription.js +0 -214
  188. package/dist/mcp/events-subscription.js.map +0 -1
  189. package/dist/mqtt/client.d.ts +0 -25
  190. package/dist/mqtt/client.js +0 -181
  191. package/dist/mqtt/client.js.map +0 -1
  192. package/dist/mqtt/credential.d.ts +0 -16
  193. package/dist/mqtt/credential.js +0 -31
  194. package/dist/mqtt/credential.js.map +0 -1
  195. package/dist/policy/add-rule.d.ts +0 -21
  196. package/dist/policy/add-rule.js +0 -125
  197. package/dist/policy/add-rule.js.map +0 -1
  198. package/dist/policy/diff.d.ts +0 -21
  199. package/dist/policy/diff.js +0 -92
  200. package/dist/policy/diff.js.map +0 -1
  201. package/dist/policy/format.d.ts +0 -6
  202. package/dist/policy/format.js +0 -58
  203. package/dist/policy/format.js.map +0 -1
  204. package/dist/policy/load.d.ts +0 -32
  205. package/dist/policy/load.js +0 -62
  206. package/dist/policy/load.js.map +0 -1
  207. package/dist/policy/migrate.d.ts +0 -21
  208. package/dist/policy/migrate.js +0 -68
  209. package/dist/policy/migrate.js.map +0 -1
  210. package/dist/policy/schema.d.ts +0 -5
  211. package/dist/policy/schema.js +0 -19
  212. package/dist/policy/schema.js.map +0 -1
  213. package/dist/policy/validate.d.ts +0 -19
  214. package/dist/policy/validate.js +0 -263
  215. package/dist/policy/validate.js.map +0 -1
  216. package/dist/rules/action.d.ts +0 -65
  217. package/dist/rules/action.js +0 -217
  218. package/dist/rules/action.js.map +0 -1
  219. package/dist/rules/audit-query.d.ts +0 -51
  220. package/dist/rules/audit-query.js +0 -90
  221. package/dist/rules/audit-query.js.map +0 -1
  222. package/dist/rules/conflict-analyzer.d.ts +0 -57
  223. package/dist/rules/conflict-analyzer.js +0 -215
  224. package/dist/rules/conflict-analyzer.js.map +0 -1
  225. package/dist/rules/cron-scheduler.d.ts +0 -62
  226. package/dist/rules/cron-scheduler.js +0 -187
  227. package/dist/rules/cron-scheduler.js.map +0 -1
  228. package/dist/rules/destructive.d.ts +0 -20
  229. package/dist/rules/destructive.js +0 -53
  230. package/dist/rules/destructive.js.map +0 -1
  231. package/dist/rules/engine.d.ts +0 -193
  232. package/dist/rules/engine.js +0 -758
  233. package/dist/rules/engine.js.map +0 -1
  234. package/dist/rules/matcher.d.ts +0 -56
  235. package/dist/rules/matcher.js +0 -231
  236. package/dist/rules/matcher.js.map +0 -1
  237. package/dist/rules/pid-file.d.ts +0 -43
  238. package/dist/rules/pid-file.js +0 -96
  239. package/dist/rules/pid-file.js.map +0 -1
  240. package/dist/rules/quiet-hours.d.ts +0 -26
  241. package/dist/rules/quiet-hours.js +0 -46
  242. package/dist/rules/quiet-hours.js.map +0 -1
  243. package/dist/rules/suggest.d.ts +0 -20
  244. package/dist/rules/suggest.js +0 -96
  245. package/dist/rules/suggest.js.map +0 -1
  246. package/dist/rules/throttle.d.ts +0 -61
  247. package/dist/rules/throttle.js +0 -117
  248. package/dist/rules/throttle.js.map +0 -1
  249. package/dist/rules/types.d.ts +0 -117
  250. package/dist/rules/types.js +0 -35
  251. package/dist/rules/types.js.map +0 -1
  252. package/dist/rules/webhook-listener.d.ts +0 -63
  253. package/dist/rules/webhook-listener.js +0 -224
  254. package/dist/rules/webhook-listener.js.map +0 -1
  255. package/dist/rules/webhook-token.d.ts +0 -50
  256. package/dist/rules/webhook-token.js +0 -91
  257. package/dist/rules/webhook-token.js.map +0 -1
  258. package/dist/schema/field-aliases.d.ts +0 -34
  259. package/dist/schema/field-aliases.js +0 -132
  260. package/dist/schema/field-aliases.js.map +0 -1
  261. package/dist/sinks/dispatcher.d.ts +0 -7
  262. package/dist/sinks/dispatcher.js +0 -13
  263. package/dist/sinks/dispatcher.js.map +0 -1
  264. package/dist/sinks/file.d.ts +0 -6
  265. package/dist/sinks/file.js +0 -20
  266. package/dist/sinks/file.js.map +0 -1
  267. package/dist/sinks/format.d.ts +0 -20
  268. package/dist/sinks/format.js +0 -57
  269. package/dist/sinks/format.js.map +0 -1
  270. package/dist/sinks/homeassistant.d.ts +0 -18
  271. package/dist/sinks/homeassistant.js +0 -45
  272. package/dist/sinks/homeassistant.js.map +0 -1
  273. package/dist/sinks/openclaw.d.ts +0 -13
  274. package/dist/sinks/openclaw.js +0 -34
  275. package/dist/sinks/openclaw.js.map +0 -1
  276. package/dist/sinks/stdout.d.ts +0 -4
  277. package/dist/sinks/stdout.js +0 -6
  278. package/dist/sinks/stdout.js.map +0 -1
  279. package/dist/sinks/telegram.d.ts +0 -11
  280. package/dist/sinks/telegram.js +0 -29
  281. package/dist/sinks/telegram.js.map +0 -1
  282. package/dist/sinks/types.d.ts +0 -13
  283. package/dist/sinks/types.js +0 -2
  284. package/dist/sinks/types.js.map +0 -1
  285. package/dist/sinks/webhook.d.ts +0 -6
  286. package/dist/sinks/webhook.js +0 -23
  287. package/dist/sinks/webhook.js.map +0 -1
  288. package/dist/status-sync/manager.d.ts +0 -48
  289. package/dist/status-sync/manager.js +0 -269
  290. package/dist/status-sync/manager.js.map +0 -1
  291. package/dist/utils/arg-parsers.d.ts +0 -16
  292. package/dist/utils/arg-parsers.js +0 -67
  293. package/dist/utils/arg-parsers.js.map +0 -1
  294. package/dist/utils/audit.d.ts +0 -69
  295. package/dist/utils/audit.js +0 -122
  296. package/dist/utils/audit.js.map +0 -1
  297. package/dist/utils/filter.d.ts +0 -81
  298. package/dist/utils/filter.js +0 -190
  299. package/dist/utils/filter.js.map +0 -1
  300. package/dist/utils/flags.d.ts +0 -72
  301. package/dist/utils/flags.js +0 -187
  302. package/dist/utils/flags.js.map +0 -1
  303. package/dist/utils/format.d.ts +0 -9
  304. package/dist/utils/format.js +0 -118
  305. package/dist/utils/format.js.map +0 -1
  306. package/dist/utils/health.d.ts +0 -48
  307. package/dist/utils/health.js +0 -102
  308. package/dist/utils/health.js.map +0 -1
  309. package/dist/utils/help-json.d.ts +0 -39
  310. package/dist/utils/help-json.js +0 -55
  311. package/dist/utils/help-json.js.map +0 -1
  312. package/dist/utils/name-resolver.d.ts +0 -26
  313. package/dist/utils/name-resolver.js +0 -138
  314. package/dist/utils/name-resolver.js.map +0 -1
  315. package/dist/utils/output.d.ts +0 -73
  316. package/dist/utils/output.js +0 -405
  317. package/dist/utils/output.js.map +0 -1
  318. package/dist/utils/quota.d.ts +0 -61
  319. package/dist/utils/quota.js +0 -228
  320. package/dist/utils/quota.js.map +0 -1
  321. package/dist/utils/redact.d.ts +0 -23
  322. package/dist/utils/redact.js +0 -69
  323. package/dist/utils/redact.js.map +0 -1
  324. package/dist/utils/retry.d.ts +0 -72
  325. package/dist/utils/retry.js +0 -141
  326. package/dist/utils/retry.js.map +0 -1
  327. package/dist/utils/string.d.ts +0 -2
  328. package/dist/utils/string.js +0 -23
  329. package/dist/utils/string.js.map +0 -1
  330. package/dist/version.d.ts +0 -2
  331. package/dist/version.js +0 -5
  332. package/dist/version.js.map +0 -1
@@ -1,654 +0,0 @@
1
- import fs from 'node:fs';
2
- import readline from 'node:readline';
3
- import { randomUUID } from 'node:crypto';
4
- import { printJson, isJsonMode, handleError, exitWithError } from '../utils/output.js';
5
- import { executeCommand, isDestructiveCommand } from '../lib/devices.js';
6
- import { executeScene } from '../lib/scenes.js';
7
- import { getCachedDevice } from '../devices/cache.js';
8
- import { resolveDeviceId } from '../utils/name-resolver.js';
9
- import { COMMAND_KEYWORDS } from '../lib/command-keywords.js';
10
- import { savePlanRecord, loadPlanRecord, updatePlanRecord, listPlanRecords, PLANS_DIR, } from '../lib/plan-store.js';
11
- import { allowsDirectDestructiveExecution, destructiveExecutionHint } from '../lib/destructive-mode.js';
12
- function findDestructivePlanSteps(plan) {
13
- const destructive = [];
14
- for (let i = 0; i < plan.steps.length; i++) {
15
- const step = plan.steps[i];
16
- if (step.type !== 'command')
17
- continue;
18
- const resolvedDeviceId = resolveDeviceId(step.deviceId, step.deviceName);
19
- const deviceType = getCachedDevice(resolvedDeviceId)?.type;
20
- const commandType = step.commandType ?? 'command';
21
- if (isDestructiveCommand(deviceType, step.command, commandType)) {
22
- destructive.push({ index: i + 1, deviceId: resolvedDeviceId, command: step.command, commandType, deviceType: deviceType ?? null });
23
- }
24
- }
25
- return destructive;
26
- }
27
- const PLAN_JSON_SCHEMA = {
28
- $schema: 'https://json-schema.org/draft/2020-12/schema',
29
- $id: 'https://switchbot.dev/plan-1.0.json',
30
- title: 'SwitchBot Plan',
31
- description: 'Declarative batch of SwitchBot operations. Agent-authored; CLI validates and executes. No LLM inside the CLI — the schema is the contract.',
32
- type: 'object',
33
- required: ['version', 'steps'],
34
- properties: {
35
- version: { const: '1.0' },
36
- description: { type: 'string' },
37
- steps: {
38
- type: 'array',
39
- items: {
40
- oneOf: [
41
- {
42
- type: 'object',
43
- required: ['type', 'command'],
44
- oneOf: [
45
- { required: ['deviceId'], not: { required: ['deviceName'] } },
46
- { required: ['deviceName'], not: { required: ['deviceId'] } },
47
- ],
48
- properties: {
49
- type: { const: 'command' },
50
- deviceId: { type: 'string', minLength: 1 },
51
- deviceName: { type: 'string', minLength: 1 },
52
- command: { type: 'string', minLength: 1 },
53
- parameter: {},
54
- commandType: { enum: ['command', 'customize'] },
55
- note: { type: 'string' },
56
- },
57
- additionalProperties: false,
58
- },
59
- {
60
- type: 'object',
61
- required: ['type', 'sceneId'],
62
- properties: {
63
- type: { const: 'scene' },
64
- sceneId: { type: 'string', minLength: 1 },
65
- note: { type: 'string' },
66
- },
67
- additionalProperties: false,
68
- },
69
- {
70
- type: 'object',
71
- required: ['type', 'ms'],
72
- properties: {
73
- type: { const: 'wait' },
74
- ms: { type: 'integer', minimum: 0, maximum: 600000 },
75
- note: { type: 'string' },
76
- },
77
- additionalProperties: false,
78
- },
79
- ],
80
- },
81
- },
82
- },
83
- additionalProperties: false,
84
- };
85
- export function validatePlan(raw) {
86
- const issues = [];
87
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
88
- return { ok: false, issues: [{ path: '$', message: 'plan must be a JSON object' }] };
89
- }
90
- const p = raw;
91
- if (p.version !== '1.0') {
92
- issues.push({ path: 'version', message: 'must equal "1.0"' });
93
- }
94
- if (!Array.isArray(p.steps)) {
95
- issues.push({ path: 'steps', message: 'must be an array' });
96
- return { ok: false, issues };
97
- }
98
- p.steps.forEach((step, i) => {
99
- const at = `steps[${i}]`;
100
- if (!step || typeof step !== 'object') {
101
- issues.push({ path: at, message: 'must be an object' });
102
- return;
103
- }
104
- const s = step;
105
- switch (s.type) {
106
- case 'command':
107
- if (s.deviceId !== undefined && (typeof s.deviceId !== 'string' || !s.deviceId)) {
108
- issues.push({ path: `${at}.deviceId`, message: 'must be a non-empty string when provided' });
109
- }
110
- if (s.deviceName !== undefined && (typeof s.deviceName !== 'string' || !s.deviceName)) {
111
- issues.push({ path: `${at}.deviceName`, message: 'must be a non-empty string when provided' });
112
- }
113
- if (!s.deviceId && !s.deviceName) {
114
- issues.push({ path: `${at}`, message: 'must have either "deviceId" or "deviceName"' });
115
- }
116
- if (s.deviceId && s.deviceName) {
117
- issues.push({ path: `${at}`, message: '"deviceId" and "deviceName" cannot both be set' });
118
- }
119
- if (typeof s.command !== 'string' || !s.command) {
120
- issues.push({ path: `${at}.command`, message: 'must be a non-empty string' });
121
- }
122
- if (s.commandType !== undefined &&
123
- s.commandType !== 'command' &&
124
- s.commandType !== 'customize') {
125
- issues.push({
126
- path: `${at}.commandType`,
127
- message: 'must be "command" or "customize"',
128
- });
129
- }
130
- break;
131
- case 'scene':
132
- if (typeof s.sceneId !== 'string' || !s.sceneId) {
133
- issues.push({ path: `${at}.sceneId`, message: 'must be a non-empty string' });
134
- }
135
- break;
136
- case 'wait':
137
- if (typeof s.ms !== 'number' || !Number.isInteger(s.ms) || s.ms < 0 || s.ms > 600_000) {
138
- issues.push({
139
- path: `${at}.ms`,
140
- message: 'must be an integer in [0, 600000]',
141
- });
142
- }
143
- break;
144
- default:
145
- issues.push({
146
- path: `${at}.type`,
147
- message: 'must be one of "command" | "scene" | "wait"',
148
- });
149
- }
150
- });
151
- if (issues.length > 0)
152
- return { ok: false, issues };
153
- return { ok: true, plan: raw };
154
- }
155
- export function suggestPlan(opts) {
156
- const warnings = [];
157
- let command = '';
158
- for (const k of COMMAND_KEYWORDS) {
159
- if (k.pattern.test(opts.intent)) {
160
- command = k.command;
161
- break;
162
- }
163
- }
164
- if (!command) {
165
- command = 'turnOn';
166
- warnings.push(`Could not infer command from intent "${opts.intent}" — defaulted to "turnOn". Edit the generated plan to set the correct command.`);
167
- }
168
- const steps = opts.devices.map((d) => ({
169
- type: 'command',
170
- deviceId: d.id,
171
- command,
172
- }));
173
- return { plan: { version: '1.0', description: opts.intent, steps }, warnings };
174
- }
175
- async function readPlanSource(file) {
176
- const text = file === undefined || file === '-'
177
- ? await readStdin()
178
- : fs.readFileSync(file, 'utf8');
179
- if (!text.trim()) {
180
- throw new Error(file === undefined || file === '-'
181
- ? 'no plan received on stdin'
182
- : `plan file is empty: ${file}`);
183
- }
184
- try {
185
- return JSON.parse(text);
186
- }
187
- catch (err) {
188
- throw new Error(`plan is not valid JSON: ${err.message}`);
189
- }
190
- }
191
- function readStdin() {
192
- return new Promise((resolve, reject) => {
193
- let buf = '';
194
- process.stdin.setEncoding('utf8');
195
- process.stdin.on('data', (chunk) => (buf += chunk));
196
- process.stdin.on('end', () => resolve(buf));
197
- process.stdin.on('error', reject);
198
- });
199
- }
200
- async function promptApproval(stepIdx, command, deviceId) {
201
- if (!process.stdin.isTTY)
202
- return false;
203
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
204
- return new Promise((resolve) => {
205
- rl.question(` Approve step ${stepIdx} — ${command} on ${deviceId}? [y/N] `, (answer) => {
206
- rl.close();
207
- resolve(answer.trim().toLowerCase() === 'y');
208
- });
209
- });
210
- }
211
- /** Shared plan-execution core used by both `plan run` and `plan execute`. */
212
- async function executePlanSteps(plan, planId, options) {
213
- const out = {
214
- plan,
215
- results: [],
216
- summary: { total: plan.steps.length, ok: 0, error: 0, skipped: 0 },
217
- };
218
- for (let i = 0; i < plan.steps.length; i++) {
219
- const step = plan.steps[i];
220
- const idx = i + 1;
221
- if (step.type === 'wait') {
222
- await new Promise((r) => setTimeout(r, step.ms));
223
- out.results.push({ step: idx, type: 'wait', ms: step.ms, status: 'ok' });
224
- out.summary.ok++;
225
- if (!isJsonMode())
226
- console.log(` ${idx}. wait ${step.ms}ms`);
227
- continue;
228
- }
229
- if (step.type === 'scene') {
230
- try {
231
- await executeScene(step.sceneId);
232
- out.results.push({ step: idx, type: 'scene', sceneId: step.sceneId, status: 'ok' });
233
- out.summary.ok++;
234
- if (!isJsonMode())
235
- console.log(` ${idx}. ✓ scene ${step.sceneId}`);
236
- }
237
- catch (err) {
238
- const msg = err instanceof Error ? err.message : String(err);
239
- out.results.push({ step: idx, type: 'scene', sceneId: step.sceneId, status: 'error', error: msg });
240
- out.summary.error++;
241
- if (!isJsonMode())
242
- console.log(` ${idx}. ✗ scene ${step.sceneId}: ${msg}`);
243
- if (!options.continueOnError)
244
- break;
245
- }
246
- continue;
247
- }
248
- const resolvedDeviceId = resolveDeviceId(step.deviceId, step.deviceName);
249
- const deviceType = getCachedDevice(resolvedDeviceId)?.type;
250
- const commandType = step.commandType ?? 'command';
251
- const destructive = isDestructiveCommand(deviceType, step.command, commandType);
252
- let approvalDecision;
253
- if (destructive && !options.yes) {
254
- if (options.requireApproval) {
255
- const approved = await promptApproval(idx, step.command, resolvedDeviceId);
256
- if (approved) {
257
- approvalDecision = 'approved';
258
- }
259
- else {
260
- out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'skipped', error: 'destructive — rejected at prompt', decision: 'rejected' });
261
- out.summary.skipped++;
262
- if (!isJsonMode())
263
- console.log(` ${idx}. ✗ skipped ${step.command} on ${resolvedDeviceId} (rejected)`);
264
- if (!options.continueOnError)
265
- break;
266
- continue;
267
- }
268
- }
269
- else {
270
- out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'skipped', error: 'destructive — rerun with --yes' });
271
- out.summary.skipped++;
272
- if (!isJsonMode())
273
- console.log(` ${idx}. ⚠ skipped ${step.command} on ${resolvedDeviceId} (destructive — pass --yes)`);
274
- if (!options.continueOnError)
275
- break;
276
- continue;
277
- }
278
- }
279
- try {
280
- await executeCommand(resolvedDeviceId, step.command, step.parameter, commandType, undefined, { planId });
281
- out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'ok', ...(approvalDecision ? { decision: approvalDecision } : {}) });
282
- out.summary.ok++;
283
- if (!isJsonMode())
284
- console.log(` ${idx}. ✓ ${step.command} on ${resolvedDeviceId}`);
285
- }
286
- catch (err) {
287
- if (err instanceof Error && err.name === 'DryRunSignal') {
288
- out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'ok' });
289
- out.summary.ok++;
290
- if (!isJsonMode())
291
- console.log(` ${idx}. ◦ dry-run ${step.command} on ${resolvedDeviceId}`);
292
- continue;
293
- }
294
- const msg = err instanceof Error ? err.message : String(err);
295
- out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'error', error: msg });
296
- out.summary.error++;
297
- if (!isJsonMode())
298
- console.log(` ${idx}. ✗ ${step.command} on ${resolvedDeviceId}: ${msg}`);
299
- if (!options.continueOnError)
300
- break;
301
- }
302
- }
303
- return out;
304
- }
305
- export function registerPlanCommand(program) {
306
- const plan = program
307
- .command('plan')
308
- .description('Author, validate, and run SwitchBot batch plans (JSON schema for AI agents)')
309
- .addHelpText('after', `
310
- A "plan" is a JSON document describing a sequence of commands/scenes/waits.
311
- The schema is fixed — agents emit plans, the CLI executes them. No LLM inside.
312
-
313
- { "version": "1.0", "description": "...", "steps": [
314
- { "type": "command", "deviceId": "...", "command": "turnOff" },
315
- { "type": "wait", "ms": 500 },
316
- { "type": "scene", "sceneId": "..." }
317
- ]}
318
-
319
- Workflow:
320
- $ switchbot plan schema > plan.schema.json # export the contract
321
- $ switchbot plan validate my-plan.json # check shape without running
322
- $ switchbot --dry-run plan run my-plan.json # preview (mutations skipped)
323
- $ switchbot plan save my-plan.json # store a reviewed plan
324
- $ switchbot plan review <planId>
325
- $ switchbot plan approve <planId>
326
- $ switchbot plan execute <planId>
327
- $ cat plan.json | switchbot plan run - # or stream via stdin
328
- `);
329
- plan
330
- .command('schema')
331
- .description('Print the JSON Schema for the plan format')
332
- .action(() => {
333
- printJson({
334
- ...PLAN_JSON_SCHEMA,
335
- agentNotes: {
336
- deviceNameStrategy: "Plan step `deviceName` fields are resolved with the `require-unique` strategy (same default as `devices command`). Plans that expect a specific device should pin `deviceId` instead.",
337
- },
338
- });
339
- });
340
- plan
341
- .command('validate')
342
- .description('Validate a plan file (or stdin) against the schema (structural only; does not verify device or scene existence)')
343
- .argument('[file]', 'Path to plan.json, or "-" / omit to read stdin')
344
- .addHelpText('after', `
345
- To check semantic validity (e.g., that deviceIds and sceneIds actually exist),
346
- use 'plan run --dry-run' which exercises name resolution and device lookup
347
- against the live API without executing any mutations.
348
- `)
349
- .action(async (file) => {
350
- let raw;
351
- try {
352
- raw = await readPlanSource(file);
353
- }
354
- catch (err) {
355
- handleError(err);
356
- }
357
- const result = validatePlan(raw);
358
- if (!result.ok) {
359
- if (isJsonMode()) {
360
- printJson({ valid: false, issues: result.issues });
361
- }
362
- else {
363
- console.error('✗ plan invalid:');
364
- for (const i of result.issues) {
365
- console.error(` ${i.path}: ${i.message}`);
366
- }
367
- }
368
- process.exit(2);
369
- }
370
- if (isJsonMode()) {
371
- const out = { valid: true, steps: result.plan.steps.length };
372
- if (result.plan.steps.length === 0)
373
- out.warning = 'plan has no steps — nothing will execute';
374
- printJson(out);
375
- }
376
- else {
377
- if (result.plan.steps.length === 0) {
378
- console.log('✓ plan valid — but 0 steps: nothing will execute');
379
- }
380
- else {
381
- console.log(`✓ plan valid (${result.plan.steps.length} step${result.plan.steps.length === 1 ? '' : 's'})`);
382
- }
383
- }
384
- });
385
- plan
386
- .command('suggest')
387
- .description('Generate a candidate Plan JSON from intent + devices (heuristic, no LLM)')
388
- .requiredOption('--intent <text>', 'Natural language description (e.g. "turn off all lights")')
389
- .option('--device <id>', 'Device ID to include (repeatable)', (v, prev) => [...prev, v], [])
390
- .option('--out <file>', 'Write plan JSON to file instead of stdout')
391
- .action((opts) => {
392
- if (opts.device.length === 0) {
393
- console.error('error: at least one --device is required');
394
- process.exit(1);
395
- }
396
- const devices = opts.device.map((ref) => {
397
- const cached = getCachedDevice(ref);
398
- return { id: ref, name: cached?.name, type: cached?.type };
399
- });
400
- const { plan: suggested, warnings } = suggestPlan({ intent: opts.intent, devices });
401
- for (const w of warnings)
402
- process.stderr.write(`warning: ${w}\n`);
403
- const json = JSON.stringify(suggested, null, 2);
404
- if (opts.out) {
405
- fs.writeFileSync(opts.out, json + '\n', 'utf8');
406
- if (!isJsonMode())
407
- console.log(`✓ plan written to ${opts.out}`);
408
- }
409
- else if (isJsonMode()) {
410
- printJson({ plan: suggested, warnings });
411
- }
412
- else {
413
- console.log(json);
414
- }
415
- });
416
- plan
417
- .command('run')
418
- .description('Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default')
419
- .argument('[file]', 'Path to plan.json, or "-" / omit to read stdin')
420
- .option('--yes', 'Authorize destructive commands (e.g. Smart Lock unlock, Garage open)')
421
- .option('--require-approval', 'Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)')
422
- .option('--continue-on-error', 'Keep running after a failed step (default: stop at first error)')
423
- .action(async (file, options) => {
424
- if (options.requireApproval && isJsonMode()) {
425
- console.error('error: --require-approval cannot be used with --json (no TTY available for prompts)');
426
- process.exit(1);
427
- }
428
- let raw;
429
- try {
430
- raw = await readPlanSource(file);
431
- }
432
- catch (err) {
433
- handleError(err);
434
- }
435
- const v = validatePlan(raw);
436
- if (!v.ok) {
437
- if (isJsonMode()) {
438
- printJson({ ran: false, issues: v.issues });
439
- }
440
- else {
441
- console.error('✗ plan invalid, refusing to run:');
442
- for (const i of v.issues)
443
- console.error(` ${i.path}: ${i.message}`);
444
- }
445
- process.exit(2);
446
- }
447
- const planId = randomUUID();
448
- const destructiveSteps = findDestructivePlanSteps(v.plan);
449
- if (options.yes && destructiveSteps.length > 0 && !allowsDirectDestructiveExecution()) {
450
- exitWithError({
451
- code: 2,
452
- kind: 'guard',
453
- message: `Direct destructive execution is disabled for plan run (${destructiveSteps.length} destructive step${destructiveSteps.length === 1 ? '' : 's'}).`,
454
- hint: destructiveExecutionHint(),
455
- context: {
456
- planId,
457
- destructiveSteps: destructiveSteps.map((step) => ({
458
- step: step.index,
459
- deviceId: step.deviceId,
460
- deviceType: step.deviceType,
461
- command: step.command,
462
- commandType: step.commandType,
463
- })),
464
- requiredWorkflow: 'plan-approval',
465
- },
466
- });
467
- }
468
- let out;
469
- try {
470
- out = await executePlanSteps(v.plan, planId, options);
471
- if (isJsonMode()) {
472
- printJson({ ran: true, planId, ...out });
473
- }
474
- else {
475
- const { ok, error, skipped, total } = out.summary;
476
- console.log(`\nsummary: ok=${ok} error=${error} skipped=${skipped} total=${total}`);
477
- }
478
- }
479
- catch (err) {
480
- handleError(err);
481
- return;
482
- }
483
- if (out.summary.error > 0)
484
- process.exit(1);
485
- });
486
- // ---- Plan resource-model subcommands (P0-3) --------------------------------
487
- plan
488
- .command('save')
489
- .description('Save a plan JSON to ~/.switchbot/plans/ with status=pending (waiting for approval).')
490
- .argument('[file]', 'Path to plan.json, or "-" / omit to read stdin')
491
- .action(async (file) => {
492
- let raw;
493
- try {
494
- raw = await readPlanSource(file);
495
- }
496
- catch (err) {
497
- handleError(err);
498
- return;
499
- }
500
- const v = validatePlan(raw);
501
- if (!v.ok) {
502
- exitWithError({
503
- code: 2, kind: 'usage',
504
- message: `Plan is invalid (${v.issues.length} issue${v.issues.length > 1 ? 's' : ''})`,
505
- context: { issues: v.issues },
506
- });
507
- }
508
- const record = savePlanRecord(v.plan);
509
- if (isJsonMode()) {
510
- printJson({ saved: true, planId: record.planId, status: record.status, createdAt: record.createdAt, plansDir: PLANS_DIR });
511
- }
512
- else {
513
- console.log(`✓ Plan saved — planId: ${record.planId}`);
514
- console.log(` Status: ${record.status}`);
515
- console.log(` Path: ${PLANS_DIR}/${record.planId}.json`);
516
- console.log(` Next: switchbot plan review ${record.planId}`);
517
- console.log(` switchbot plan approve ${record.planId}`);
518
- }
519
- });
520
- plan
521
- .command('list')
522
- .description('List saved plans in ~/.switchbot/plans/ with their approval status.')
523
- .action(() => {
524
- const records = listPlanRecords();
525
- if (isJsonMode()) {
526
- printJson({ plans: records.map((r) => ({ planId: r.planId, status: r.status, createdAt: r.createdAt, approvedAt: r.approvedAt ?? null, executedAt: r.executedAt ?? null, description: r.plan.description ?? null })) });
527
- return;
528
- }
529
- if (records.length === 0) {
530
- console.log('No saved plans. Use: switchbot plan save <file>');
531
- return;
532
- }
533
- for (const r of records) {
534
- const parts = [`${r.planId.slice(0, 8)}…`, r.status, r.createdAt.slice(0, 16)];
535
- if (r.plan.description)
536
- parts.push(`"${r.plan.description}"`);
537
- console.log(parts.join(' '));
538
- }
539
- });
540
- plan
541
- .command('review')
542
- .description('Show the details of a saved plan (steps, status, approval history).')
543
- .argument('<planId>', 'Plan UUID from "plan list"')
544
- .action((planId) => {
545
- const record = loadPlanRecord(planId);
546
- if (!record) {
547
- exitWithError({ code: 2, kind: 'usage', message: `Plan ${planId} not found in ${PLANS_DIR}` });
548
- }
549
- if (isJsonMode()) {
550
- printJson(record);
551
- return;
552
- }
553
- console.log(`planId: ${record.planId}`);
554
- console.log(`status: ${record.status}`);
555
- console.log(`createdAt: ${record.createdAt}`);
556
- if (record.approvedAt)
557
- console.log(`approvedAt: ${record.approvedAt}`);
558
- if (record.executedAt)
559
- console.log(`executedAt: ${record.executedAt}`);
560
- if (record.plan.description)
561
- console.log(`description: ${record.plan.description}`);
562
- console.log(`steps (${record.plan.steps.length}):`);
563
- for (let i = 0; i < record.plan.steps.length; i++) {
564
- const step = record.plan.steps[i];
565
- if (step.type === 'command') {
566
- const id = step.deviceId ?? step.deviceName ?? '?';
567
- console.log(` ${i + 1}. command ${step.command} on ${id}${step.note ? ` # ${step.note}` : ''}`);
568
- }
569
- else if (step.type === 'scene') {
570
- console.log(` ${i + 1}. scene ${step.sceneId}${step.note ? ` # ${step.note}` : ''}`);
571
- }
572
- else {
573
- console.log(` ${i + 1}. wait ${step.ms}ms`);
574
- }
575
- }
576
- });
577
- plan
578
- .command('approve')
579
- .description('Approve a saved plan, allowing `plan execute` to run it.')
580
- .argument('<planId>', 'Plan UUID from "plan list"')
581
- .action((planId) => {
582
- const record = loadPlanRecord(planId);
583
- if (!record) {
584
- exitWithError({ code: 2, kind: 'usage', message: `Plan ${planId} not found in ${PLANS_DIR}` });
585
- }
586
- if (record.status === 'executed') {
587
- exitWithError({ code: 2, kind: 'guard', message: `Plan ${planId} has already been executed.` });
588
- }
589
- if (record.status === 'rejected') {
590
- exitWithError({ code: 2, kind: 'guard', message: `Plan ${planId} was rejected. Save a new plan to start fresh.` });
591
- }
592
- // 'failed' plans may be re-approved and retried — intentionally no block here.
593
- const updated = updatePlanRecord(planId, { status: 'approved', approvedAt: new Date().toISOString() });
594
- if (isJsonMode()) {
595
- printJson({ ok: true, planId: updated.planId, status: updated.status, approvedAt: updated.approvedAt });
596
- }
597
- else {
598
- console.log(`✓ Plan ${planId.slice(0, 8)}… approved.`);
599
- console.log(` Next: switchbot plan execute ${planId}`);
600
- }
601
- });
602
- plan
603
- .command('execute')
604
- .description('Execute a pre-approved plan. Only runs if status=approved; audit entries are tagged with planId.')
605
- .argument('<planId>', 'Plan UUID from "plan list" (must be in approved status)')
606
- .option('--yes', 'Deprecated no-op: approved plans already authorize destructive steps')
607
- .option('--require-approval', 'Prompt for each destructive step (TTY only)')
608
- .option('--continue-on-error', 'Keep running after a failed step')
609
- .action(async (planId, options) => {
610
- if (options.requireApproval && isJsonMode()) {
611
- exitWithError({ code: 1, kind: 'usage', message: '--require-approval cannot be used with --json' });
612
- }
613
- const record = loadPlanRecord(planId);
614
- if (!record) {
615
- exitWithError({ code: 2, kind: 'usage', message: `Plan ${planId} not found in ${PLANS_DIR}` });
616
- }
617
- if (record.status !== 'approved') {
618
- exitWithError({
619
- code: 2, kind: 'guard',
620
- message: `Plan ${planId.slice(0, 8)}… cannot be executed: status is "${record.status}", expected "approved".`,
621
- hint: record.status === 'pending' ? `Run: switchbot plan approve ${planId}` : record.status === 'failed' ? `Re-run: switchbot plan approve ${planId}` : undefined,
622
- context: { planId, status: record.status },
623
- });
624
- }
625
- let out;
626
- try {
627
- out = await executePlanSteps(record.plan, planId, { ...options, yes: true });
628
- }
629
- catch (err) {
630
- handleError(err);
631
- return;
632
- }
633
- const { ok, error, skipped } = out.summary;
634
- const succeeded = error === 0 && skipped === 0;
635
- const failureReason = succeeded ? undefined : [error > 0 ? `${error} error${error > 1 ? 's' : ''}` : null, skipped > 0 ? `${skipped} skipped` : null].filter(Boolean).join(', ');
636
- if (succeeded) {
637
- updatePlanRecord(planId, { status: 'executed', executedAt: new Date().toISOString() });
638
- }
639
- else {
640
- updatePlanRecord(planId, { status: 'failed', failedAt: new Date().toISOString(), failureReason });
641
- }
642
- if (isJsonMode()) {
643
- printJson({ ran: true, planId, succeeded, ...out });
644
- }
645
- else {
646
- console.log(`\nsummary: ok=${ok} error=${error} skipped=${skipped} total=${out.summary.total}`);
647
- if (!succeeded)
648
- console.error(`Plan marked as failed (${failureReason}). Re-run after fixing to retry.`);
649
- }
650
- if (!succeeded)
651
- process.exit(1);
652
- });
653
- }
654
- //# sourceMappingURL=plan.js.map