@switchbot/openapi-cli 3.1.1 → 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 +3 -3
  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 -410
  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 -107
  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 -216
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -214
  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 -121
  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,264 +0,0 @@
1
- import { printJson, isJsonMode, handleError, StructuredUsageError } from '../utils/output.js';
2
- import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
3
- import { fetchScenes, executeScene } from '../lib/scenes.js';
4
- import { isDryRun } from '../utils/flags.js';
5
- export function registerScenesCommand(program) {
6
- const scenes = program
7
- .command('scenes')
8
- .description('Manage and execute SwitchBot scenes');
9
- // switchbot scenes list
10
- scenes
11
- .command('list')
12
- .description('List all manual scenes (scenes created in the SwitchBot app)')
13
- .addHelpText('after', `
14
- Output columns: sceneId, sceneName
15
- --fields accepts any subset of these names (exit 2 on unknown names).
16
-
17
- Examples:
18
- $ switchbot scenes list
19
- $ switchbot scenes list --format tsv --fields sceneId,sceneName
20
- $ switchbot scenes list --format id
21
- $ switchbot scenes list --json
22
- `)
23
- .action(async () => {
24
- try {
25
- const scenes = await fetchScenes();
26
- const fmt = resolveFormat();
27
- if (fmt === 'json' && process.argv.includes('--json')) {
28
- printJson(scenes);
29
- return;
30
- }
31
- renderRows(['sceneId', 'sceneName'], scenes.map((s) => [s.sceneId, s.sceneName]), fmt, resolveFields(), { id: 'sceneId', name: 'sceneName' });
32
- if (fmt === 'table' && scenes.length === 0) {
33
- console.log('No scenes found');
34
- }
35
- }
36
- catch (error) {
37
- handleError(error);
38
- }
39
- });
40
- // switchbot scenes execute <sceneId>
41
- scenes
42
- .command('execute')
43
- .description('Execute a manual scene by its ID')
44
- .argument('<sceneId>', 'Scene ID from "scenes list"')
45
- .addHelpText('after', `
46
- Example:
47
- $ switchbot scenes execute T12345678
48
- `)
49
- .action(async (sceneId) => {
50
- try {
51
- const sceneList = await fetchScenes();
52
- const found = sceneList.find((s) => s.sceneId === sceneId);
53
- if (!found) {
54
- throw new StructuredUsageError(`scene not found: ${sceneId}`, {
55
- error: 'scene_not_found',
56
- sceneId,
57
- candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
58
- });
59
- }
60
- if (isDryRun()) {
61
- const wouldSend = { method: 'POST', url: `/v1.1/scenes/${sceneId}/execute`, sceneId, sceneName: found.sceneName };
62
- if (isJsonMode()) {
63
- printJson({ dryRun: true, wouldSend });
64
- }
65
- else {
66
- console.log(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
67
- }
68
- return;
69
- }
70
- await executeScene(sceneId);
71
- if (isJsonMode()) {
72
- printJson({ ok: true, sceneId });
73
- }
74
- else {
75
- console.log(`✓ Scene executed: ${sceneId}`);
76
- }
77
- }
78
- catch (error) {
79
- handleError(error);
80
- }
81
- });
82
- // switchbot scenes describe <sceneId>
83
- scenes
84
- .command('describe')
85
- .description('Show metadata for a scene by its ID (SwitchBot API v1.1 does not expose step detail)')
86
- .argument('<sceneId>', 'Scene ID from "scenes list"')
87
- .addHelpText('after', `
88
- Note: SwitchBot API v1.1 does not return scene step detail. Only the scene name is available.
89
-
90
- Example:
91
- $ switchbot scenes describe T12345678
92
- `)
93
- .action(async (sceneId) => {
94
- try {
95
- const sceneList = await fetchScenes();
96
- const found = sceneList.find((s) => s.sceneId === sceneId);
97
- if (!found) {
98
- throw new StructuredUsageError(`scene not found: ${sceneId}`, {
99
- error: 'scene_not_found',
100
- sceneId,
101
- candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
102
- });
103
- }
104
- const result = {
105
- sceneId: found.sceneId,
106
- sceneName: found.sceneName,
107
- stepCount: null,
108
- note: 'SwitchBot API v1.1 does not expose scene steps — displayed name only',
109
- };
110
- if (isJsonMode()) {
111
- printJson(result);
112
- }
113
- else {
114
- console.log(`sceneId: ${result.sceneId}`);
115
- console.log(`sceneName: ${result.sceneName}`);
116
- console.log(`stepCount: (not available)`);
117
- console.log(`note: ${result.note}`);
118
- }
119
- }
120
- catch (error) {
121
- handleError(error);
122
- }
123
- });
124
- // switchbot scenes validate [sceneId...]
125
- scenes
126
- .command('validate')
127
- .description('Verify that one or more scenes exist. If no IDs are given, validates all scenes are reachable.')
128
- .argument('[sceneId...]', 'Scene IDs to validate (default: all scenes)')
129
- .addHelpText('after', `
130
- Note: SwitchBot API v1.1 does not expose scene steps; validation only confirms
131
- the scene IDs exist in your account.
132
-
133
- Examples:
134
- $ switchbot scenes validate
135
- $ switchbot scenes validate T12345678 T87654321
136
- `)
137
- .action(async (sceneIds) => {
138
- try {
139
- const sceneList = await fetchScenes();
140
- const sceneMap = new Map(sceneList.map((s) => [s.sceneId, s.sceneName]));
141
- const targets = sceneIds.length > 0 ? sceneIds : sceneList.map((s) => s.sceneId);
142
- const results = targets.map((id) => ({
143
- sceneId: id,
144
- sceneName: sceneMap.get(id) ?? null,
145
- valid: sceneMap.has(id),
146
- }));
147
- const allValid = results.every((r) => r.valid);
148
- if (isJsonMode()) {
149
- printJson({ ok: allValid, results });
150
- if (!allValid)
151
- process.exit(1);
152
- return;
153
- }
154
- for (const r of results) {
155
- const icon = r.valid ? '✓' : '✗';
156
- const label = r.valid ? r.sceneName : '(not found)';
157
- console.log(`${icon} ${r.sceneId} ${label}`);
158
- }
159
- if (!allValid)
160
- process.exit(1);
161
- }
162
- catch (error) {
163
- handleError(error);
164
- }
165
- });
166
- // switchbot scenes simulate <sceneId>
167
- scenes
168
- .command('simulate')
169
- .description('Show what `scenes execute` would do without actually executing the scene.')
170
- .argument('<sceneId>', 'Scene ID from "scenes list"')
171
- .addHelpText('after', `
172
- Note: SwitchBot API v1.1 does not expose scene step details. Simulation reports
173
- the scene name, confirms it exists, and shows the POST that would be issued.
174
-
175
- Example:
176
- $ switchbot scenes simulate T12345678
177
- `)
178
- .action(async (sceneId) => {
179
- try {
180
- const sceneList = await fetchScenes();
181
- const found = sceneList.find((s) => s.sceneId === sceneId);
182
- if (!found) {
183
- throw new StructuredUsageError(`scene not found: ${sceneId}`, {
184
- error: 'scene_not_found',
185
- sceneId,
186
- candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
187
- });
188
- }
189
- const simulation = {
190
- sceneId: found.sceneId,
191
- sceneName: found.sceneName,
192
- wouldSend: { method: 'POST', url: `/v1.1/scenes/${sceneId}/execute` },
193
- note: 'SwitchBot API v1.1 does not expose individual scene steps.',
194
- };
195
- if (isJsonMode()) {
196
- printJson({ simulated: true, ...simulation });
197
- return;
198
- }
199
- console.log(`sceneId: ${simulation.sceneId}`);
200
- console.log(`sceneName: ${simulation.sceneName}`);
201
- console.log(`wouldSend: ${simulation.wouldSend.method} ${simulation.wouldSend.url}`);
202
- console.log(`note: ${simulation.note}`);
203
- }
204
- catch (error) {
205
- handleError(error);
206
- }
207
- });
208
- // switchbot scenes explain <sceneId>
209
- scenes
210
- .command('explain')
211
- .description('Explain in plain language what a scene does and how to execute it safely.')
212
- .argument('<sceneId>', 'Scene ID from "scenes list"')
213
- .addHelpText('after', `
214
- Shows the scene name, action description, risk level, and the exact command to
215
- run. Unlike "simulate" (which shows raw HTTP detail), "explain" is aimed at a
216
- human or agent deciding whether to proceed.
217
-
218
- Note: SwitchBot API v1.1 does not expose scene step details; risk is reported
219
- as "low" because scenes only trigger pre-configured automations in the app.
220
-
221
- Example:
222
- $ switchbot scenes explain T12345678
223
- `)
224
- .action(async (sceneId) => {
225
- try {
226
- const sceneList = await fetchScenes();
227
- const found = sceneList.find((s) => s.sceneId === sceneId);
228
- if (!found) {
229
- throw new StructuredUsageError(`scene not found: ${sceneId}`, {
230
- error: 'scene_not_found',
231
- sceneId,
232
- candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
233
- });
234
- }
235
- const explanation = {
236
- sceneId: found.sceneId,
237
- sceneName: found.sceneName,
238
- action: `Trigger scene (POST /v1.1/scenes/${found.sceneId}/execute)`,
239
- riskLevel: 'low',
240
- idempotent: null,
241
- toExecute: `switchbot scenes execute ${found.sceneId}`,
242
- dryRun: isDryRun(),
243
- note: 'SwitchBot API v1.1 does not expose individual scene steps.',
244
- };
245
- if (isJsonMode()) {
246
- printJson(explanation);
247
- return;
248
- }
249
- console.log(`sceneId: ${explanation.sceneId}`);
250
- console.log(`sceneName: ${explanation.sceneName}`);
251
- console.log(`action: ${explanation.action}`);
252
- console.log(`riskLevel: ${explanation.riskLevel}`);
253
- console.log(`idempotent: unknown (scene steps not exposed by API)`);
254
- console.log(`toExecute: ${explanation.toExecute}`);
255
- if (explanation.dryRun) {
256
- console.log(`dryRun: true (pass --dry-run to execute would be a no-op)`);
257
- }
258
- console.log(`note: ${explanation.note}`);
259
- }
260
- catch (error) {
261
- handleError(error);
262
- }
263
- });
264
- }
@@ -1,177 +0,0 @@
1
- import { enumArg, stringArg } from '../utils/arg-parsers.js';
2
- import { printJson } from '../utils/output.js';
3
- import { getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
4
- import { RESOURCE_CATALOG } from '../devices/resources.js';
5
- import { loadCache } from '../devices/cache.js';
6
- function toSchemaEntry(e) {
7
- return {
8
- type: e.type,
9
- description: e.description ?? '',
10
- category: e.category,
11
- aliases: e.aliases ?? [],
12
- role: e.role ?? null,
13
- readOnly: e.readOnly ?? false,
14
- commands: e.commands.map((c) => toSchemaCommand(c, e)),
15
- statusFields: e.statusFields ?? [],
16
- };
17
- }
18
- function toSchemaCommand(c, entry) {
19
- const tier = deriveSafetyTier(c, entry);
20
- const reason = getCommandSafetyReason(c);
21
- return {
22
- command: c.command,
23
- parameter: c.parameter,
24
- description: c.description,
25
- commandType: (c.commandType ?? 'command'),
26
- idempotent: Boolean(c.idempotent),
27
- safetyTier: tier,
28
- ...(reason ? { safetyReason: reason } : {}),
29
- ...(c.exampleParams ? { exampleParams: c.exampleParams } : {}),
30
- };
31
- }
32
- function toCompactEntry(e) {
33
- return {
34
- type: e.type,
35
- category: e.category,
36
- role: e.role ?? null,
37
- readOnly: e.readOnly ?? false,
38
- commands: e.commands.map((c) => {
39
- const tier = deriveSafetyTier(c, e);
40
- return {
41
- command: c.command,
42
- parameter: c.parameter,
43
- commandType: (c.commandType ?? 'command'),
44
- idempotent: Boolean(c.idempotent),
45
- safetyTier: tier,
46
- };
47
- }),
48
- statusFields: e.statusFields ?? [],
49
- };
50
- }
51
- function projectFields(entry, fields) {
52
- const out = {};
53
- for (const f of fields) {
54
- if (f in entry)
55
- out[f] = entry[f];
56
- }
57
- return out;
58
- }
59
- export function registerSchemaCommand(program) {
60
- const ROLES = ['lighting', 'security', 'sensor', 'climate', 'media', 'cleaning', 'curtain', 'fan', 'power', 'hub', 'other'];
61
- const CATEGORIES = ['physical', 'ir'];
62
- const schema = program
63
- .command('schema')
64
- .description('Export the SwitchBot device catalog as structured JSON (for AI agent prompts / tooling)');
65
- schema
66
- .command('export')
67
- .description('Print the catalog as structured JSON (one object per type)')
68
- .option('--type <type>', 'Restrict to a single device type (e.g. "Strip Light")', stringArg('--type'))
69
- .option('--types <csv>', 'Restrict to multiple device types (comma-separated)', stringArg('--types'))
70
- .option('--role <role>', 'Restrict to a functional role: lighting, security, sensor, climate, media, cleaning, curtain, fan, power, hub, other', enumArg('--role', ROLES))
71
- .option('--category <cat>', 'Restrict to "physical" or "ir"', enumArg('--category', CATEGORIES))
72
- .option('--compact', 'Drop descriptions/aliases/example params — emit ~60% smaller payload. Useful for agent prompts.')
73
- .option('--used', 'Restrict to device types present in the local devices cache (run "devices list" first)')
74
- .option('--project <csv>', 'Project per-type fields (e.g. --project type,commands,statusFields)', stringArg('--project'))
75
- .addHelpText('after', `
76
- Output is always JSON (this command ignores --format). The output is a
77
- catalog export — not a formal JSON Schema standard document — suitable for
78
- pre-baking LLM prompts or regenerating docs when the catalog changes.
79
-
80
- Size tips:
81
- --compact --used Smallest realistic payload for a given account
82
- (< 15 KB on most accounts).
83
- --fields type,commands Strip statusFields / role / etc. when only
84
- commands are needed.
85
- --type + --compact Inspect one type with minimum footprint.
86
-
87
- Common top-level fields:
88
- schemaVersion CLI schema version (stable for agent contracts)
89
- data.version Catalog schema version
90
- data.types Array of SchemaEntry (or CompactSchemaEntry with --compact)
91
- data._fetchedAt CLI-added; present on live-query responses ('devices status'),
92
- not on this offline export.
93
-
94
- Examples:
95
- $ switchbot schema export > catalog.json
96
- $ switchbot schema export --compact --used | wc -c # small prompt-ready payload
97
- $ switchbot schema export --type Bot | jq '.data.types[0].commands'
98
- $ switchbot schema export --types "Bot,Curtain,Color Bulb"
99
- $ switchbot schema export --role lighting | jq '[.data.types[].type]'
100
- $ switchbot schema export --role security --category physical
101
- $ switchbot schema export --project type,commands,statusFields
102
- `)
103
- .action((options) => {
104
- const catalog = getEffectiveCatalog();
105
- let filtered = catalog;
106
- if (options.type) {
107
- const q = options.type.toLowerCase();
108
- filtered = filtered.filter((e) => e.type.toLowerCase() === q ||
109
- (e.aliases ?? []).some((a) => a.toLowerCase() === q));
110
- }
111
- if (options.types) {
112
- const set = new Set(options.types.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
113
- filtered = filtered.filter((e) => set.has(e.type.toLowerCase()) ||
114
- (e.aliases ?? []).some((a) => set.has(a.toLowerCase())));
115
- }
116
- if (options.role) {
117
- const q = options.role.toLowerCase();
118
- filtered = filtered.filter((e) => (e.role ?? 'other') === q);
119
- }
120
- if (options.category) {
121
- const q = options.category.toLowerCase();
122
- filtered = filtered.filter((e) => e.category === q);
123
- }
124
- if (options.used) {
125
- const cache = loadCache();
126
- if (cache) {
127
- const usedTypes = new Set(Object.values(cache.devices).map((d) => d.type.toLowerCase()));
128
- filtered = filtered.filter((e) => usedTypes.has(e.type.toLowerCase()) ||
129
- (e.aliases ?? []).some((a) => usedTypes.has(a.toLowerCase())));
130
- }
131
- else {
132
- filtered = [];
133
- }
134
- }
135
- const mapped = options.compact
136
- ? filtered.map(toCompactEntry)
137
- : filtered.map(toSchemaEntry);
138
- const projected = options.project
139
- ? mapped.map((e) => projectFields(e, options.project.split(',').map((s) => s.trim()).filter(Boolean)))
140
- : mapped;
141
- const payload = {
142
- version: '1.0',
143
- types: projected,
144
- };
145
- if (!options.compact) {
146
- payload.generatedAt = new Date().toISOString();
147
- payload.resources = RESOURCE_CATALOG;
148
- payload.cliAddedFields = [
149
- {
150
- field: '_fetchedAt',
151
- appliesTo: ['devices status', 'devices describe'],
152
- type: 'string (ISO-8601)',
153
- description: 'CLI-synthesized timestamp indicating when this status response was fetched or served from the cache. Not part of the upstream SwitchBot API.',
154
- },
155
- {
156
- field: 'replayed',
157
- appliesTo: ['devices command (with --idempotency-key)'],
158
- type: 'boolean',
159
- description: 'CLI-synthesized flag — true when the response was served from the idempotency cache instead of re-executing the command.',
160
- },
161
- {
162
- field: 'verification',
163
- appliesTo: ['devices command'],
164
- type: 'object',
165
- description: 'CLI-synthesized receipt-acknowledgment metadata. For IR devices, verifiable:false signals that no device-side confirmation is possible.',
166
- },
167
- {
168
- field: 'hints',
169
- appliesTo: ['agent-bootstrap'],
170
- type: 'string[]',
171
- description: 'CLI-synthesized advisory messages for the calling agent. Always emitted; empty array ([]) means no hints to report — never null and not a disabled-field signal.',
172
- },
173
- ];
174
- }
175
- printJson(payload);
176
- });
177
- }
@@ -1,131 +0,0 @@
1
- import { stringArg } from '../utils/arg-parsers.js';
2
- import { handleError, isJsonMode, printJson } from '../utils/output.js';
3
- import { getStatusSyncStatus, runStatusSyncForeground, startStatusSync, stopStatusSync, } from '../status-sync/manager.js';
4
- function printHumanStatus(status) {
5
- if (!status.running) {
6
- console.log('status-sync is not running');
7
- console.log(`state: ${status.stateDir}`);
8
- console.log(`stdout: ${status.stdoutLog}`);
9
- console.log(`stderr: ${status.stderrLog}`);
10
- return;
11
- }
12
- console.log(`status-sync is running (PID ${status.pid})`);
13
- console.log(`started: ${status.startedAt}`);
14
- console.log(`state: ${status.stateDir}`);
15
- console.log(`stdout: ${status.stdoutLog}`);
16
- console.log(`stderr: ${status.stderrLog}`);
17
- }
18
- export function registerStatusSyncCommand(program) {
19
- const statusSync = program
20
- .command('status-sync')
21
- .description('Manage a background MQTT -> OpenClaw status-sync bridge powered by events mqtt-tail');
22
- statusSync
23
- .command('run')
24
- .description('Run the status-sync bridge in the foreground for a supervisor or terminal session')
25
- .option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)', stringArg('--openclaw-url'))
26
- .option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)', stringArg('--openclaw-token'))
27
- .option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to (or env OPENCLAW_MODEL)', stringArg('--openclaw-model'))
28
- .option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)', stringArg('--topic'))
29
- .addHelpText('after', `
30
- Runs the same MQTT -> OpenClaw bridge logic as \'status-sync start\',
31
- but keeps the process attached to the current terminal. This is the best fit
32
- for agent supervisors, service managers, or container entrypoints that want
33
- foreground process semantics.
34
-
35
- Examples:
36
- $ switchbot status-sync run --openclaw-model home-agent
37
- $ OPENCLAW_TOKEN=abc OPENCLAW_MODEL=home-agent switchbot status-sync run
38
- `)
39
- .action(async (options) => {
40
- try {
41
- const exitCode = await runStatusSyncForeground(options);
42
- if (exitCode !== 0) {
43
- process.exit(exitCode);
44
- }
45
- }
46
- catch (error) {
47
- handleError(error);
48
- }
49
- });
50
- statusSync
51
- .command('start')
52
- .description('Start the background status-sync bridge')
53
- .option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)', stringArg('--openclaw-url'))
54
- .option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)', stringArg('--openclaw-token'))
55
- .option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to (or env OPENCLAW_MODEL)', stringArg('--openclaw-model'))
56
- .option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)', stringArg('--topic'))
57
- .option('--state-dir <path>', 'Override the status-sync state directory (or env SWITCHBOT_STATUS_SYNC_HOME)', stringArg('--state-dir'))
58
- .option('--force', 'Stop any existing status-sync bridge before starting a new one')
59
- .addHelpText('after', `
60
- Starts a detached child process that runs:
61
- switchbot status-sync run ...
62
-
63
- State files:
64
- state.json process metadata (pid, startedAt, command)
65
- stdout.log redirected stdout from the child process
66
- stderr.log redirected stderr from the child process
67
-
68
- Examples:
69
- $ switchbot status-sync start --openclaw-model home-agent
70
- $ OPENCLAW_TOKEN=abc OPENCLAW_MODEL=home-agent switchbot status-sync start
71
- $ switchbot status-sync start --state-dir ~/.switchbot/custom-status-sync --force
72
- `)
73
- .action((options) => {
74
- try {
75
- const status = startStatusSync(options);
76
- if (isJsonMode()) {
77
- printJson(status);
78
- return;
79
- }
80
- console.log(`Started status-sync (PID ${status.pid}).`);
81
- console.log(`state: ${status.stateDir}`);
82
- console.log(`stdout: ${status.stdoutLog}`);
83
- console.log(`stderr: ${status.stderrLog}`);
84
- }
85
- catch (error) {
86
- handleError(error);
87
- }
88
- });
89
- statusSync
90
- .command('stop')
91
- .description('Stop the background status-sync bridge')
92
- .option('--state-dir <path>', 'Override the status-sync state directory (or env SWITCHBOT_STATUS_SYNC_HOME)', stringArg('--state-dir'))
93
- .action((options) => {
94
- try {
95
- const result = stopStatusSync(options);
96
- if (isJsonMode()) {
97
- printJson(result);
98
- return;
99
- }
100
- if (result.stopped) {
101
- console.log(`Stopped status-sync (PID ${result.pid}).`);
102
- }
103
- else if (result.stale) {
104
- console.log(`Removed stale status-sync state for PID ${result.pid}.`);
105
- }
106
- else {
107
- console.log('status-sync is not running');
108
- }
109
- }
110
- catch (error) {
111
- handleError(error);
112
- }
113
- });
114
- statusSync
115
- .command('status')
116
- .description('Inspect the current status-sync bridge state')
117
- .option('--state-dir <path>', 'Override the status-sync state directory (or env SWITCHBOT_STATUS_SYNC_HOME)', stringArg('--state-dir'))
118
- .action((options) => {
119
- try {
120
- const status = getStatusSyncStatus(options);
121
- if (isJsonMode()) {
122
- printJson(status);
123
- return;
124
- }
125
- printHumanStatus(status);
126
- }
127
- catch (error) {
128
- handleError(error);
129
- }
130
- });
131
- }