@switchbot/openapi-cli 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +34 -42
  2. package/dist/index.js +56945 -169
  3. package/dist/policy/schema/v0.2.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/api/client.js +0 -235
  6. package/dist/auth.js +0 -20
  7. package/dist/commands/agent-bootstrap.js +0 -182
  8. package/dist/commands/auth.js +0 -354
  9. package/dist/commands/batch.js +0 -413
  10. package/dist/commands/cache.js +0 -126
  11. package/dist/commands/capabilities.js +0 -385
  12. package/dist/commands/catalog.js +0 -359
  13. package/dist/commands/completion.js +0 -385
  14. package/dist/commands/config.js +0 -376
  15. package/dist/commands/daemon.js +0 -367
  16. package/dist/commands/device-meta.js +0 -159
  17. package/dist/commands/devices.js +0 -948
  18. package/dist/commands/doctor.js +0 -1015
  19. package/dist/commands/events.js +0 -563
  20. package/dist/commands/expand.js +0 -130
  21. package/dist/commands/explain.js +0 -139
  22. package/dist/commands/health.js +0 -113
  23. package/dist/commands/history.js +0 -320
  24. package/dist/commands/identity.js +0 -59
  25. package/dist/commands/install.js +0 -246
  26. package/dist/commands/mcp.js +0 -2017
  27. package/dist/commands/plan.js +0 -653
  28. package/dist/commands/policy.js +0 -586
  29. package/dist/commands/quota.js +0 -78
  30. package/dist/commands/rules.js +0 -875
  31. package/dist/commands/scenes.js +0 -264
  32. package/dist/commands/schema.js +0 -177
  33. package/dist/commands/status-sync.js +0 -131
  34. package/dist/commands/uninstall.js +0 -237
  35. package/dist/commands/upgrade-check.js +0 -88
  36. package/dist/commands/watch.js +0 -194
  37. package/dist/commands/webhook.js +0 -182
  38. package/dist/config.js +0 -258
  39. package/dist/credentials/backends/file.js +0 -101
  40. package/dist/credentials/backends/linux.js +0 -129
  41. package/dist/credentials/backends/macos.js +0 -129
  42. package/dist/credentials/backends/windows.js +0 -215
  43. package/dist/credentials/keychain.js +0 -88
  44. package/dist/credentials/prime.js +0 -52
  45. package/dist/devices/cache.js +0 -293
  46. package/dist/devices/catalog.js +0 -767
  47. package/dist/devices/device-meta.js +0 -56
  48. package/dist/devices/history-agg.js +0 -138
  49. package/dist/devices/history-query.js +0 -181
  50. package/dist/devices/param-validator.js +0 -433
  51. package/dist/devices/resources.js +0 -270
  52. package/dist/install/default-steps.js +0 -257
  53. package/dist/install/preflight.js +0 -212
  54. package/dist/install/steps.js +0 -67
  55. package/dist/lib/command-keywords.js +0 -17
  56. package/dist/lib/daemon-state.js +0 -46
  57. package/dist/lib/destructive-mode.js +0 -12
  58. package/dist/lib/devices.js +0 -382
  59. package/dist/lib/idempotency.js +0 -106
  60. package/dist/lib/plan-store.js +0 -68
  61. package/dist/lib/request-context.js +0 -12
  62. package/dist/lib/scenes.js +0 -10
  63. package/dist/logger.js +0 -16
  64. package/dist/mcp/device-history.js +0 -145
  65. package/dist/mcp/events-subscription.js +0 -213
  66. package/dist/mqtt/client.js +0 -180
  67. package/dist/mqtt/credential.js +0 -30
  68. package/dist/policy/add-rule.js +0 -124
  69. package/dist/policy/diff.js +0 -91
  70. package/dist/policy/format.js +0 -57
  71. package/dist/policy/load.js +0 -61
  72. package/dist/policy/migrate.js +0 -67
  73. package/dist/policy/schema.js +0 -18
  74. package/dist/policy/validate.js +0 -262
  75. package/dist/rules/action.js +0 -205
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -203
  78. package/dist/rules/cron-scheduler.js +0 -186
  79. package/dist/rules/destructive.js +0 -52
  80. package/dist/rules/engine.js +0 -757
  81. package/dist/rules/matcher.js +0 -230
  82. package/dist/rules/pid-file.js +0 -95
  83. package/dist/rules/quiet-hours.js +0 -45
  84. package/dist/rules/suggest.js +0 -95
  85. package/dist/rules/throttle.js +0 -116
  86. package/dist/rules/types.js +0 -34
  87. package/dist/rules/webhook-listener.js +0 -223
  88. package/dist/rules/webhook-token.js +0 -90
  89. package/dist/schema/field-aliases.js +0 -131
  90. package/dist/sinks/dispatcher.js +0 -12
  91. package/dist/sinks/file.js +0 -19
  92. package/dist/sinks/format.js +0 -56
  93. package/dist/sinks/homeassistant.js +0 -44
  94. package/dist/sinks/openclaw.js +0 -33
  95. package/dist/sinks/stdout.js +0 -5
  96. package/dist/sinks/telegram.js +0 -28
  97. package/dist/sinks/types.js +0 -1
  98. package/dist/sinks/webhook.js +0 -22
  99. package/dist/status-sync/manager.js +0 -268
  100. package/dist/utils/arg-parsers.js +0 -66
  101. package/dist/utils/audit.js +0 -117
  102. package/dist/utils/filter.js +0 -189
  103. package/dist/utils/flags.js +0 -186
  104. package/dist/utils/format.js +0 -117
  105. package/dist/utils/health.js +0 -101
  106. package/dist/utils/help-json.js +0 -54
  107. package/dist/utils/name-resolver.js +0 -137
  108. package/dist/utils/output.js +0 -404
  109. package/dist/utils/quota.js +0 -227
  110. package/dist/utils/redact.js +0 -68
  111. package/dist/utils/retry.js +0 -140
  112. package/dist/utils/string.js +0 -22
  113. package/dist/version.js +0 -4
@@ -1,376 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import readline from 'node:readline';
5
- import { execFileSync } from 'node:child_process';
6
- import { stringArg } from '../utils/arg-parsers.js';
7
- import { intArg } from '../utils/arg-parsers.js';
8
- import { saveConfig, showConfig, getConfigSummary, listProfiles, readProfileMeta } from '../config.js';
9
- import { isJsonMode, printJson, exitWithError } from '../utils/output.js';
10
- import chalk from 'chalk';
11
- function parseEnvFile(file) {
12
- const out = {};
13
- const raw = fs.readFileSync(file, 'utf-8');
14
- for (const line of raw.split(/\r?\n/)) {
15
- const trimmed = line.trim();
16
- if (!trimmed || trimmed.startsWith('#'))
17
- continue;
18
- const eq = trimmed.indexOf('=');
19
- if (eq === -1)
20
- continue;
21
- const key = trimmed.slice(0, eq).trim();
22
- let val = trimmed.slice(eq + 1).trim();
23
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
24
- val = val.slice(1, -1);
25
- }
26
- if (key === 'SWITCHBOT_TOKEN')
27
- out.token = val;
28
- else if (key === 'SWITCHBOT_SECRET')
29
- out.secret = val;
30
- }
31
- return out;
32
- }
33
- function readFromOp(ref) {
34
- // 1Password CLI: `op read "op://vault/item/field"` → single line on stdout
35
- const stdout = execFileSync('op', ['read', ref], { encoding: 'utf-8' });
36
- return stdout.trim();
37
- }
38
- // Replace raw token/secret positional slots in process.argv with "***" so
39
- // neither verbose traces nor crash dumps nor any later inspector observe them.
40
- function scrubArgvCredentials() {
41
- const argv = process.argv;
42
- for (let i = 2; i < argv.length - 2; i++) {
43
- if (argv[i] === 'config' && argv[i + 1] === 'set-token') {
44
- // Slots i+2 and i+3 (if not option flags) are token/secret.
45
- for (const off of [2, 3]) {
46
- const slot = i + off;
47
- if (slot < argv.length && !argv[slot].startsWith('-')) {
48
- argv[slot] = '***';
49
- }
50
- }
51
- return;
52
- }
53
- }
54
- }
55
- async function promptSecret(question) {
56
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
57
- const stdoutAny = process.stdout;
58
- const mutableStdout = process.stderr;
59
- return new Promise((resolve) => {
60
- process.stderr.write(question);
61
- const stdin = process.stdin;
62
- let answer = '';
63
- const onData = (chunk) => {
64
- const s = chunk.toString('utf-8');
65
- for (const ch of s) {
66
- if (ch === '\r' || ch === '\n') {
67
- stdin.removeListener('data', onData);
68
- if (stdin.setRawMode)
69
- stdin.setRawMode(false);
70
- stdin.pause();
71
- process.stderr.write('\n');
72
- rl.close();
73
- resolve(answer);
74
- return;
75
- }
76
- if (ch === '\u0003') {
77
- process.exit(130);
78
- }
79
- if (ch === '\u007f' || ch === '\b') {
80
- answer = answer.slice(0, -1);
81
- continue;
82
- }
83
- answer += ch;
84
- }
85
- };
86
- if (stdin.setRawMode)
87
- stdin.setRawMode(true);
88
- stdin.resume();
89
- stdin.on('data', onData);
90
- void stdoutAny;
91
- void mutableStdout;
92
- });
93
- }
94
- /**
95
- * Interactive echo-off prompt for token + secret. Used by both
96
- * `switchbot config set-token` and the install orchestrator. Throws if
97
- * stdin is not a TTY.
98
- */
99
- export async function promptTokenAndSecret() {
100
- if (!process.stdin.isTTY) {
101
- throw new Error('interactive prompt requires a TTY');
102
- }
103
- const token = (await promptSecret('Token: ')).trim();
104
- const secret = (await promptSecret('Secret: ')).trim();
105
- if (!token || !secret) {
106
- throw new Error('token and secret are both required');
107
- }
108
- return { token, secret };
109
- }
110
- /**
111
- * Read a two-line credential file (line 1 = token, line 2 = secret)
112
- * and unlink it on success. The installer's `--token-file` escape
113
- * hatch uses this; keeps credentials off the command line and shell
114
- * history for CI-style installs.
115
- */
116
- export function readCredentialsFile(filePath) {
117
- const raw = fs.readFileSync(filePath, 'utf-8');
118
- const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
119
- if (lines.length < 2) {
120
- throw new Error(`credential file ${filePath} must contain two lines: token, then secret`);
121
- }
122
- return { token: lines[0].trim(), secret: lines[1].trim() };
123
- }
124
- export function registerConfigCommand(program) {
125
- const config = program
126
- .command('config')
127
- .description('Manage SwitchBot API credentials')
128
- .addHelpText('after', `
129
- Credential priority:
130
- 1. Environment variables: SWITCHBOT_TOKEN and SWITCHBOT_SECRET
131
- 2. --config <path> (explicit file override)
132
- 3. --profile <name> → ~/.switchbot/profiles/<name>.json
133
- 4. ~/.switchbot/config.json (default)
134
-
135
- Obtain your token/secret from the SwitchBot mobile app:
136
- Profile → Preferences → Developer Options → Get Token
137
- `);
138
- config
139
- .command('set-token')
140
- .description('Save token and secret (mode 0600). Use --profile to target a named profile.')
141
- .argument('[token]', 'API token; omit when using --from-env-file / --from-op')
142
- .argument('[secret]', 'API client secret; omit when using --from-env-file / --from-op')
143
- .option('--from-env-file <path>', 'Read SWITCHBOT_TOKEN and SWITCHBOT_SECRET from a dotenv file', stringArg('--from-env-file'))
144
- .option('--from-op <tokenRef>', 'Read token via 1Password CLI (op read). Pair with --op-secret <ref>', stringArg('--from-op'))
145
- .option('--op-secret <secretRef>', '1Password reference for the secret, used with --from-op', stringArg('--op-secret'))
146
- .option('--label <text>', 'Human-friendly label for this profile (shown in config show / list-profiles)', stringArg('--label'))
147
- .option('--description <text>', 'Longer description, e.g. "home account" or "work devices"', stringArg('--description'))
148
- .option('--daily-cap <n>', 'Local cap on SwitchBot API calls per UTC day for this profile', intArg('--daily-cap', { min: 1 }))
149
- .option('--default-flags <csv>', 'Comma-separated flags auto-applied for this profile (e.g. "--audit-log")', stringArg('--default-flags'))
150
- .addHelpText('after', `
151
- Examples:
152
- # Interactive (recommended) — credentials never touch shell history / ps listing
153
- $ switchbot config set-token
154
- Token: ****
155
- Secret: ****
156
-
157
- # Import from dotenv / 1Password (non-interactive, still safe)
158
- $ switchbot config set-token --from-env-file ./.env
159
- $ switchbot config set-token --from-op op://vault/switchbot/token --op-secret op://vault/switchbot/secret
160
-
161
- # Advanced / non-interactive (DISCOURAGED — leaks to shell history)
162
- $ switchbot config set-token <token> <secret>
163
- $ switchbot --profile work config set-token <token> <secret>
164
-
165
- Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<name>.json.
166
- `)
167
- .action(async (tokenArg, secretArg, options) => {
168
- let token = tokenArg;
169
- let secret = secretArg;
170
- const hadPositional = tokenArg !== undefined && secretArg !== undefined;
171
- // Scrub early: commander has already parsed the values, so we can safely
172
- // rewrite argv before anything else (verbose trace, crash dumps, …) sees it.
173
- if (hadPositional) {
174
- scrubArgvCredentials();
175
- console.error('⚠ Passing token/secret as positional arguments is discouraged — they may be persisted in shell history, process listings, and agent logs.');
176
- console.error(' Prefer: switchbot config set-token (interactive), --from-env-file, or --from-op.');
177
- }
178
- if (options.fromEnvFile) {
179
- if (!fs.existsSync(options.fromEnvFile)) {
180
- exitWithError({
181
- code: 2,
182
- kind: 'usage',
183
- message: `--from-env-file: file not found: ${options.fromEnvFile}`,
184
- });
185
- }
186
- const parsed = parseEnvFile(options.fromEnvFile);
187
- token = token ?? parsed.token;
188
- secret = secret ?? parsed.secret;
189
- }
190
- if (options.fromOp) {
191
- if (!options.opSecret) {
192
- exitWithError({
193
- code: 2,
194
- kind: 'usage',
195
- message: '--from-op requires --op-secret <ref> for the secret reference.',
196
- });
197
- }
198
- try {
199
- token = readFromOp(options.fromOp);
200
- secret = readFromOp(options.opSecret);
201
- }
202
- catch (err) {
203
- exitWithError({
204
- code: 1,
205
- kind: 'runtime',
206
- message: `1Password CLI read failed: ${err instanceof Error ? err.message : String(err)}`,
207
- hint: 'Ensure the "op" CLI is installed and authenticated (op signin).',
208
- });
209
- }
210
- }
211
- // No credentials yet and stdin is a TTY → interactive prompt (safest path).
212
- if ((!token || !secret) && !options.fromEnvFile && !options.fromOp && process.stdin.isTTY) {
213
- if (isJsonMode()) {
214
- exitWithError({
215
- code: 2,
216
- kind: 'usage',
217
- message: 'Interactive mode cannot run under --json. Provide token/secret via --from-env-file, --from-op, or positional args.',
218
- });
219
- }
220
- try {
221
- if (!token)
222
- token = (await promptSecret('Token: ')).trim();
223
- if (!secret)
224
- secret = (await promptSecret('Secret: ')).trim();
225
- }
226
- catch {
227
- console.error('Interactive prompt failed.');
228
- process.exit(1);
229
- }
230
- }
231
- if (!token || !secret) {
232
- exitWithError({
233
- code: 2,
234
- kind: 'usage',
235
- message: 'Missing token/secret. Run interactively, or use --from-env-file / --from-op, or pass positional arguments (discouraged).',
236
- });
237
- }
238
- saveConfig(token, secret, {
239
- label: options.label,
240
- description: options.description,
241
- limits: options.dailyCap ? { dailyCap: Number.parseInt(options.dailyCap, 10) } : undefined,
242
- defaults: options.defaultFlags
243
- ? {
244
- flags: options.defaultFlags
245
- .split(',')
246
- .map((s) => s.trim())
247
- .filter(Boolean),
248
- }
249
- : undefined,
250
- });
251
- if (isJsonMode()) {
252
- printJson({ ok: true, message: 'credentials saved' });
253
- }
254
- else {
255
- console.log(chalk.green('✓ Credentials saved'));
256
- // Keychain-first hint: proactively suggest storing in OS keychain when
257
- // the user is on a supported platform and saved to file (default path).
258
- try {
259
- const { selectCredentialStore } = await import('../credentials/keychain.js');
260
- const store = await selectCredentialStore();
261
- if (store.describe().backend === 'file') {
262
- const platform = process.platform;
263
- if (platform === 'darwin' || platform === 'win32') {
264
- console.error(chalk.grey('Tip: Your OS supports a native keychain. Run `switchbot auth keychain store` to move credentials off the plain config file for better security.'));
265
- }
266
- else if (platform === 'linux') {
267
- console.error(chalk.grey('Tip: If you have GNOME Keyring (secret-tool) installed, run `switchbot auth keychain store` to store credentials more securely.'));
268
- }
269
- }
270
- }
271
- catch {
272
- // Keychain probe failed — silently skip the tip
273
- }
274
- }
275
- });
276
- config
277
- .command('show')
278
- .description('Show the current credential source and a masked secret')
279
- .action(() => {
280
- if (isJsonMode()) {
281
- printJson(getConfigSummary());
282
- return;
283
- }
284
- showConfig();
285
- });
286
- config
287
- .command('list-profiles')
288
- .description('List named profiles under ~/.switchbot/profiles/ (with labels and daily caps)')
289
- .action(() => {
290
- const profiles = listProfiles();
291
- const enriched = profiles.map((p) => {
292
- const meta = readProfileMeta(p);
293
- return {
294
- name: p,
295
- label: meta?.label,
296
- description: meta?.description,
297
- dailyCap: meta?.limits?.dailyCap,
298
- };
299
- });
300
- if (isJsonMode()) {
301
- printJson({ profiles: enriched });
302
- return;
303
- }
304
- if (profiles.length === 0) {
305
- console.log('No profiles. Create one with: switchbot --profile <name> config set-token ...');
306
- return;
307
- }
308
- for (const p of enriched) {
309
- const bits = [p.name];
310
- if (p.label)
311
- bits.push(`— ${p.label}`);
312
- if (p.dailyCap)
313
- bits.push(`[dailyCap=${p.dailyCap}]`);
314
- console.log(bits.join(' '));
315
- }
316
- });
317
- // switchbot config agent-profile [--write]
318
- config
319
- .command('agent-profile')
320
- .description('Emit (or write) an agent-safe profile template with conservative rate limits and audit logging.')
321
- .option('--write', 'Write the template to ~/.switchbot/profiles/agent.json (requires --profile agent config set-token to add credentials).')
322
- .option('--force', 'Overwrite an existing agent.json when used with --write.')
323
- .addHelpText('after', `
324
- Outputs a starter profile.json suitable for AI agent / MCP integration:
325
- - dailyCap: 100 (conservative; prevents runaway automation)
326
- - label: "agent"
327
- - description: "AI agent profile — conservative limits + audit enabled"
328
- - defaults: { auditLog: true } (enables audit logging by default)
329
-
330
- After writing, add credentials:
331
- $ switchbot --profile agent config set-token <token> <secret>
332
-
333
- Then use the profile:
334
- $ switchbot --profile agent devices list
335
- `)
336
- .action((opts) => {
337
- const template = {
338
- label: 'agent',
339
- description: 'AI agent profile — conservative limits + audit enabled',
340
- limits: {
341
- dailyCap: 100,
342
- },
343
- defaults: {
344
- auditLog: true,
345
- },
346
- };
347
- if (opts.write) {
348
- const dir = path.join(os.homedir(), '.switchbot', 'profiles');
349
- const dest = path.join(dir, 'agent.json');
350
- if (!opts.force && fs.existsSync(dest)) {
351
- exitWithError({ code: 2, kind: 'usage', message: `Agent profile already exists: ${dest}. Use --force to overwrite.` });
352
- }
353
- if (!fs.existsSync(dir)) {
354
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
355
- }
356
- fs.writeFileSync(dest, JSON.stringify(template, null, 2), { mode: 0o600 });
357
- if (isJsonMode()) {
358
- printJson({ ok: true, path: dest, template });
359
- }
360
- else {
361
- console.log(`Agent profile written: ${dest}`);
362
- console.log(`Next: switchbot --profile agent config set-token <token> <secret>`);
363
- }
364
- }
365
- else {
366
- if (isJsonMode()) {
367
- printJson(template);
368
- }
369
- else {
370
- console.log(JSON.stringify(template, null, 2));
371
- console.log('');
372
- console.log('Write with: switchbot config agent-profile --write');
373
- }
374
- }
375
- });
376
- }