@switchbot/openapi-cli 2.6.4 → 2.7.2
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 +2 -2
- package/dist/api/client.js +13 -12
- package/dist/commands/agent-bootstrap.js +21 -15
- package/dist/commands/batch.js +26 -21
- package/dist/commands/capabilities.js +29 -21
- package/dist/commands/catalog.js +4 -3
- package/dist/commands/config.js +27 -37
- package/dist/commands/devices.js +64 -37
- package/dist/commands/doctor.js +355 -19
- package/dist/commands/events.js +112 -23
- package/dist/commands/expand.js +7 -15
- package/dist/commands/explain.js +10 -6
- package/dist/commands/history.js +12 -18
- package/dist/commands/identity.js +59 -0
- package/dist/commands/mcp.js +100 -13
- package/dist/commands/plan.js +1 -1
- package/dist/commands/schema.js +22 -12
- package/dist/commands/watch.js +15 -2
- package/dist/devices/catalog.js +124 -11
- package/dist/devices/resources.js +270 -0
- package/dist/index.js +16 -3
- package/dist/lib/devices.js +16 -5
- package/dist/schema/field-aliases.js +95 -0
- package/dist/utils/help-json.js +54 -0
- package/dist/utils/output.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
[](https://github.com/OpenWonderLabs/switchbot-openapi-cli/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
**SwitchBot** smart home CLI — control lights, locks, curtains, sensors, plugs, and IR appliances (TV/AC/fan) via the [SwitchBot Cloud API v1.1](https://github.com/OpenWonderLabs/SwitchBotAPI).
|
|
10
|
+
Run scenes, stream real-time events over MQTT, and plug AI agents into your home via the built-in MCP server — all from your terminal or shell scripts.
|
|
11
11
|
|
|
12
12
|
- **npm package:** [`@switchbot/openapi-cli`](https://www.npmjs.com/package/@switchbot/openapi-cli)
|
|
13
13
|
- **Source code:** [github.com/OpenWonderLabs/switchbot-openapi-cli](https://github.com/OpenWonderLabs/switchbot-openapi-cli)
|
package/dist/api/client.js
CHANGED
|
@@ -87,6 +87,15 @@ export function createClient() {
|
|
|
87
87
|
}
|
|
88
88
|
throw new DryRunSignal(method, url);
|
|
89
89
|
}
|
|
90
|
+
// P8: record the quota attempt BEFORE the request is dispatched so
|
|
91
|
+
// failures (timeouts / DNS errors / 5xx / aborted) also count. Only
|
|
92
|
+
// pre-flight refusals (daily-cap, --dry-run) above skip recording
|
|
93
|
+
// since they never touch the network. Retries re-enter this
|
|
94
|
+
// interceptor and record again, which matches the SwitchBot API
|
|
95
|
+
// billing model (every dispatched HTTP request consumes quota).
|
|
96
|
+
if (quotaEnabled) {
|
|
97
|
+
recordRequest(method, url);
|
|
98
|
+
}
|
|
90
99
|
return config;
|
|
91
100
|
});
|
|
92
101
|
// Handle API-level errors (HTTP 200 but statusCode !== 100)
|
|
@@ -94,11 +103,6 @@ export function createClient() {
|
|
|
94
103
|
if (verbose) {
|
|
95
104
|
process.stderr.write(chalk.grey(`[verbose] ${response.status} ${response.statusText}\n`));
|
|
96
105
|
}
|
|
97
|
-
if (quotaEnabled && response.config) {
|
|
98
|
-
const method = (response.config.method ?? 'get').toUpperCase();
|
|
99
|
-
const url = `${response.config.baseURL ?? ''}${response.config.url ?? ''}`;
|
|
100
|
-
recordRequest(method, url);
|
|
101
|
-
}
|
|
102
106
|
const data = response.data;
|
|
103
107
|
if (data.statusCode !== undefined && data.statusCode !== 100) {
|
|
104
108
|
const msg = API_ERROR_MESSAGES[data.statusCode] ??
|
|
@@ -161,13 +165,10 @@ export function createClient() {
|
|
|
161
165
|
return sleep(delay).then(() => client.request(config));
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const url = `${config.baseURL ?? ''}${config.url ?? ''}`;
|
|
169
|
-
recordRequest(method, url);
|
|
170
|
-
}
|
|
168
|
+
// P8: quota already recorded in the request interceptor before
|
|
169
|
+
// dispatch — no extra bookkeeping needed here on the error path.
|
|
170
|
+
// Timeouts, DNS failures, 5xx, and exhausted retries all counted
|
|
171
|
+
// when the attempt was first made.
|
|
171
172
|
if (status === 401) {
|
|
172
173
|
throw new ApiError('Authentication failed: invalid token or daily 10,000-request quota exceeded', 401, {
|
|
173
174
|
transient: false,
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { printJson } from '../utils/output.js';
|
|
2
2
|
import { loadCache } from '../devices/cache.js';
|
|
3
|
-
import { getEffectiveCatalog } from '../devices/catalog.js';
|
|
3
|
+
import { getEffectiveCatalog, deriveSafetyTier, CATALOG_SCHEMA_VERSION, } from '../devices/catalog.js';
|
|
4
4
|
import { readProfileMeta } from '../config.js';
|
|
5
5
|
import { todayUsage, DAILY_QUOTA } from '../utils/quota.js';
|
|
6
6
|
import { ALL_STRATEGIES } from '../utils/name-resolver.js';
|
|
7
|
+
import { IDENTITY } from './identity.js';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
const { version: pkgVersion } = require('../../package.json');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Schema version of the agent-bootstrap payload. Must stay in lockstep
|
|
13
|
+
* with the catalog schema — bootstrap consumers (AI agents) reason about
|
|
14
|
+
* catalog-derived fields (safetyTier, destructive flag), so a drift
|
|
15
|
+
* between the two would silently break their assumptions. `doctor`
|
|
16
|
+
* fails the `catalog-schema` check when these differ.
|
|
17
|
+
*/
|
|
18
|
+
export const AGENT_BOOTSTRAP_SCHEMA_VERSION = CATALOG_SCHEMA_VERSION;
|
|
17
19
|
const SAFETY_TIERS = {
|
|
18
20
|
read: 'No state mutation; safe to call freely.',
|
|
19
21
|
action: 'Mutates device/cloud state but reversible (turnOn, setColor).',
|
|
@@ -83,17 +85,21 @@ Examples:
|
|
|
83
85
|
category: e.category,
|
|
84
86
|
role: e.role ?? null,
|
|
85
87
|
readOnly: e.readOnly ?? false,
|
|
86
|
-
commands: e.commands.map((c) =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
commands: e.commands.map((c) => {
|
|
89
|
+
const tier = deriveSafetyTier(c, e);
|
|
90
|
+
return {
|
|
91
|
+
command: c.command,
|
|
92
|
+
parameter: c.parameter,
|
|
93
|
+
safetyTier: tier,
|
|
94
|
+
destructive: tier === 'destructive',
|
|
95
|
+
idempotent: Boolean(c.idempotent),
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
92
98
|
statusFields: e.statusFields ?? [],
|
|
93
99
|
};
|
|
94
100
|
});
|
|
95
101
|
const payload = {
|
|
96
|
-
schemaVersion:
|
|
102
|
+
schemaVersion: AGENT_BOOTSTRAP_SCHEMA_VERSION,
|
|
97
103
|
generatedAt: new Date().toISOString(),
|
|
98
104
|
cliVersion: pkgVersion,
|
|
99
105
|
identity: IDENTITY,
|
package/dist/commands/batch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { intArg, enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
2
|
-
import { printJson, isJsonMode, handleError, buildErrorPayload, UsageError,
|
|
2
|
+
import { printJson, isJsonMode, handleError, buildErrorPayload, UsageError, exitWithError } from '../utils/output.js';
|
|
3
3
|
import { fetchDeviceList, executeCommand, isDestructiveCommand, buildHubLocationMap, } from '../lib/devices.js';
|
|
4
4
|
import { createClient } from '../api/client.js';
|
|
5
5
|
import { parseFilter, applyFilter, FilterSyntaxError } from '../utils/filter.js';
|
|
@@ -96,7 +96,8 @@ export function registerBatchCommand(devices) {
|
|
|
96
96
|
.option('--concurrency <n>', 'Max parallel in-flight requests (default 5)', intArg('--concurrency', { min: 1 }), '5')
|
|
97
97
|
.option('--max-concurrent <n>', 'Alias for --concurrency; takes priority when set', intArg('--max-concurrent', { min: 1 }))
|
|
98
98
|
.option('--stagger <ms>', 'Fixed delay between task starts in ms (default 0 = random 20-60ms jitter)', intArg('--stagger', { min: 0 }), '0')
|
|
99
|
-
.option('--plan', 'With --dry-run: emit a plan JSON document instead of executing anything')
|
|
99
|
+
.option('--plan', '[DEPRECATED, use --emit-plan] With --dry-run: emit a plan JSON document instead of executing anything')
|
|
100
|
+
.option('--emit-plan', 'With --dry-run: emit a plan JSON document instead of executing anything')
|
|
100
101
|
.option('--yes', 'Allow destructive commands (Smart Lock unlock, garage open, ...)')
|
|
101
102
|
.option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
|
|
102
103
|
.option('--stdin', 'Read deviceIds from stdin, one per line (same as trailing "-")')
|
|
@@ -127,8 +128,9 @@ Concurrency & pacing:
|
|
|
127
128
|
--stagger <ms> Fixed delay between task starts; default 0 uses random 20-60ms jitter.
|
|
128
129
|
|
|
129
130
|
Planning:
|
|
130
|
-
--dry-run --plan
|
|
131
|
+
--dry-run --emit-plan Print the plan JSON without executing anything. Useful
|
|
131
132
|
for agents that want to show the user what will run.
|
|
133
|
+
(--plan is the deprecated alias, removed in v3.0.)
|
|
132
134
|
|
|
133
135
|
Safety:
|
|
134
136
|
Destructive commands (Smart Lock unlock, Garage Door Opener turnOn/turnOff,
|
|
@@ -146,6 +148,17 @@ Examples:
|
|
|
146
148
|
// Trailing "-" sentinel selects stdin mode.
|
|
147
149
|
const extra = commandObj.args ?? [];
|
|
148
150
|
const readStdin = Boolean(options.stdin) || extra.includes('-');
|
|
151
|
+
// P12: --plan is deprecated in favor of --emit-plan. Reject both
|
|
152
|
+
// together (conflicting) and warn when only the old flag is used.
|
|
153
|
+
if (options.plan && options.emitPlan) {
|
|
154
|
+
handleError(new UsageError('Use --emit-plan; --plan is deprecated and cannot be combined with --emit-plan.'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (options.plan && !options.emitPlan) {
|
|
158
|
+
// Warning goes to stderr so it cannot corrupt --json output on stdout.
|
|
159
|
+
console.error('[WARN] --plan is deprecated; use --emit-plan. Will be removed in v3.0.');
|
|
160
|
+
}
|
|
161
|
+
const emitPlan = Boolean(options.emitPlan || options.plan);
|
|
149
162
|
// Accept --idempotency-key as alias; reject when both forms are supplied.
|
|
150
163
|
if (options.idempotencyKey !== undefined && options.idempotencyKeyPrefix !== undefined) {
|
|
151
164
|
handleError(new UsageError('Use either --idempotency-key or --idempotency-key-prefix, not both.'));
|
|
@@ -216,22 +229,14 @@ Examples:
|
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
231
|
if (blockedForDestructive.length > 0 && !options.yes) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
console.error(`Refusing to run destructive command "${cmd}" on ${blockedForDestructive.length} device(s) without --yes:`);
|
|
231
|
-
for (const b of blockedForDestructive)
|
|
232
|
-
console.error(` ${b.deviceId}`);
|
|
233
|
-
}
|
|
234
|
-
process.exit(2);
|
|
232
|
+
const deviceIds = blockedForDestructive.map((b) => b.deviceId);
|
|
233
|
+
exitWithError({
|
|
234
|
+
code: 2,
|
|
235
|
+
kind: 'guard',
|
|
236
|
+
message: `Refusing to run destructive command "${cmd}" on ${blockedForDestructive.length} device(s) without --yes: ${deviceIds.join(', ')}`,
|
|
237
|
+
hint: 'Re-issue the call with --yes to proceed.',
|
|
238
|
+
context: { command: cmd, deviceIds },
|
|
239
|
+
});
|
|
235
240
|
}
|
|
236
241
|
// parameter may be a JSON object string; mirror the single-command action.
|
|
237
242
|
let parsedParam = parameter ?? 'default';
|
|
@@ -247,8 +252,8 @@ Examples:
|
|
|
247
252
|
const concurrency = Math.max(1, Number.parseInt(maxConcurrentRaw, 10) || DEFAULT_CONCURRENCY);
|
|
248
253
|
const staggerMs = Math.max(0, Number.parseInt(options.stagger, 10) || 0);
|
|
249
254
|
const dryRun = isDryRun();
|
|
250
|
-
// --dry-run --plan: emit a plan document and return without executing.
|
|
251
|
-
if (dryRun &&
|
|
255
|
+
// --dry-run --emit-plan (or legacy --plan): emit a plan document and return without executing.
|
|
256
|
+
if (dryRun && emitPlan) {
|
|
252
257
|
const steps = resolved.ids.map((id) => ({
|
|
253
258
|
deviceId: id,
|
|
254
259
|
command: cmd,
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import { getEffectiveCatalog } from '../devices/catalog.js';
|
|
1
|
+
import { getEffectiveCatalog, deriveSafetyTier, deriveStatusQueries, } from '../devices/catalog.js';
|
|
2
|
+
import { RESOURCE_CATALOG } from '../devices/resources.js';
|
|
2
3
|
import { loadCache } from '../devices/cache.js';
|
|
3
4
|
import { printJson } from '../utils/output.js';
|
|
4
5
|
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
6
|
+
import { IDENTITY } from './identity.js';
|
|
7
|
+
/** Collect the distinct catalog safety tiers actually used across the given entries. Sorted. */
|
|
8
|
+
function collectSafetyTiersInUse(entries) {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
for (const e of entries) {
|
|
11
|
+
for (const c of e.commands) {
|
|
12
|
+
seen.add(deriveSafetyTier(c, e));
|
|
13
|
+
}
|
|
14
|
+
// P11: statusQueries contribute the 'read' tier.
|
|
15
|
+
if (deriveStatusQueries(e).length > 0) {
|
|
16
|
+
seen.add('read');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...seen].sort();
|
|
20
|
+
}
|
|
21
|
+
/** P11: total number of read-only queries exposed across the catalog. */
|
|
22
|
+
function countStatusQueries(entries) {
|
|
23
|
+
return entries.reduce((n, e) => n + deriveStatusQueries(e).length, 0);
|
|
24
|
+
}
|
|
5
25
|
const AGENT_GUIDE = {
|
|
6
26
|
safetyTiers: {
|
|
7
27
|
read: 'No state mutation; safe to call freely — does not consume quota unless noted.',
|
|
@@ -68,23 +88,6 @@ const COMMAND_META = {
|
|
|
68
88
|
function metaFor(command) {
|
|
69
89
|
return COMMAND_META[command] ?? null;
|
|
70
90
|
}
|
|
71
|
-
const IDENTITY = {
|
|
72
|
-
product: 'SwitchBot',
|
|
73
|
-
domain: 'IoT smart home device control',
|
|
74
|
-
vendor: 'Wonderlabs, Inc.',
|
|
75
|
-
apiVersion: 'v1.1',
|
|
76
|
-
apiDocs: 'https://github.com/OpenWonderLabs/SwitchBotAPI',
|
|
77
|
-
deviceCategories: {
|
|
78
|
-
physical: 'Wi-Fi/BLE devices controllable via Cloud API (Hub required for BLE-only)',
|
|
79
|
-
ir: 'IR remote devices learned by a SwitchBot Hub (TV, AC, etc.)',
|
|
80
|
-
},
|
|
81
|
-
constraints: {
|
|
82
|
-
quotaPerDay: 10000,
|
|
83
|
-
bleRequiresHub: true,
|
|
84
|
-
authMethod: 'HMAC-SHA256 token+secret',
|
|
85
|
-
},
|
|
86
|
-
agentGuide: 'docs/agent-guide.md',
|
|
87
|
-
};
|
|
88
91
|
const MCP_TOOLS = [
|
|
89
92
|
'list_devices',
|
|
90
93
|
'get_device_status',
|
|
@@ -147,7 +150,7 @@ export function registerCapabilitiesCommand(program) {
|
|
|
147
150
|
const SURFACES = ['cli', 'mcp', 'plan', 'mqtt', 'all'];
|
|
148
151
|
program
|
|
149
152
|
.command('capabilities')
|
|
150
|
-
.description('Print a machine-readable manifest of CLI capabilities (for agent bootstrap)')
|
|
153
|
+
.description('Print a machine-readable manifest of SwitchBot CLI capabilities (for AI agent bootstrap)')
|
|
151
154
|
.option('--minimal', 'Omit per-subcommand flag details to reduce output size (alias of --compact)')
|
|
152
155
|
.option('--compact', 'Emit a compact summary: identity + leaf command list with safety metadata only')
|
|
153
156
|
.option('--used', 'Restrict the catalog summary to device types present in the local cache. Mirrors `schema export --used`.')
|
|
@@ -249,9 +252,12 @@ export function registerCapabilitiesCommand(program) {
|
|
|
249
252
|
catalog: {
|
|
250
253
|
typeCount: catalog.length,
|
|
251
254
|
roles,
|
|
252
|
-
destructiveCommandCount: catalog.reduce((n, e) => n + e.commands.filter((c) => c
|
|
255
|
+
destructiveCommandCount: catalog.reduce((n, e) => n + e.commands.filter((c) => deriveSafetyTier(c, e) === 'destructive').length, 0),
|
|
256
|
+
safetyTiersInUse: collectSafetyTiersInUse(catalog),
|
|
253
257
|
readOnlyTypeCount: catalog.filter((e) => e.readOnly).length,
|
|
258
|
+
readOnlyQueryCount: countStatusQueries(catalog),
|
|
254
259
|
},
|
|
260
|
+
resources: RESOURCE_CATALOG,
|
|
255
261
|
};
|
|
256
262
|
if (!compact)
|
|
257
263
|
payload.generatedAt = new Date().toISOString();
|
|
@@ -273,8 +279,10 @@ export function registerCapabilitiesCommand(program) {
|
|
|
273
279
|
payload.catalog = {
|
|
274
280
|
typeCount: filteredCatalog.length,
|
|
275
281
|
roles: [...new Set(filteredCatalog.map((e) => e.role ?? 'other'))].sort(),
|
|
276
|
-
destructiveCommandCount: filteredCatalog.reduce((n, e) => n + e.commands.filter((c) => c
|
|
282
|
+
destructiveCommandCount: filteredCatalog.reduce((n, e) => n + e.commands.filter((c) => deriveSafetyTier(c, e) === 'destructive').length, 0),
|
|
283
|
+
safetyTiersInUse: collectSafetyTiersInUse(filteredCatalog),
|
|
277
284
|
readOnlyTypeCount: filteredCatalog.filter((e) => e.readOnly).length,
|
|
285
|
+
readOnlyQueryCount: countStatusQueries(filteredCatalog),
|
|
278
286
|
};
|
|
279
287
|
payload.usedFilter = { applied: true, typesInCache: [...seen].sort() };
|
|
280
288
|
}
|
package/dist/commands/catalog.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { enumArg } from '../utils/arg-parsers.js';
|
|
2
2
|
import { printTable, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
3
3
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
4
|
-
import { DEVICE_CATALOG, findCatalogEntry, getCatalogOverlayPath, getEffectiveCatalog, loadCatalogOverlay, resetCatalogOverlayCache, } from '../devices/catalog.js';
|
|
4
|
+
import { DEVICE_CATALOG, findCatalogEntry, getCatalogOverlayPath, getEffectiveCatalog, loadCatalogOverlay, resetCatalogOverlayCache, deriveSafetyTier, } from '../devices/catalog.js';
|
|
5
5
|
export function registerCatalogCommand(program) {
|
|
6
6
|
const SOURCES = ['built-in', 'overlay', 'effective'];
|
|
7
7
|
const catalog = program
|
|
8
8
|
.command('catalog')
|
|
9
|
-
.description('Inspect the
|
|
9
|
+
.description('Inspect the SwitchBot device catalog (supported device types + any local overlay)')
|
|
10
10
|
.addHelpText('after', `
|
|
11
11
|
This CLI ships with a static catalog of known SwitchBot device types and
|
|
12
12
|
their commands (see 'switchbot devices types'). You can extend or override
|
|
@@ -341,10 +341,11 @@ function renderEntry(entry) {
|
|
|
341
341
|
else {
|
|
342
342
|
console.log('\nCommands:');
|
|
343
343
|
const rows = entry.commands.map((c) => {
|
|
344
|
+
const tier = deriveSafetyTier(c, entry);
|
|
344
345
|
const flags = [];
|
|
345
346
|
if (c.commandType === 'customize')
|
|
346
347
|
flags.push('customize');
|
|
347
|
-
if (
|
|
348
|
+
if (tier === 'destructive')
|
|
348
349
|
flags.push('!destructive');
|
|
349
350
|
const label = flags.length > 0 ? `${c.command} [${flags.join(', ')}]` : c.command;
|
|
350
351
|
return [label, c.parameter, c.description];
|
package/dist/commands/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { stringArg } from '../utils/arg-parsers.js';
|
|
5
5
|
import { intArg } from '../utils/arg-parsers.js';
|
|
6
6
|
import { saveConfig, showConfig, getConfigSummary, listProfiles, readProfileMeta } from '../config.js';
|
|
7
|
-
import { isJsonMode, printJson,
|
|
7
|
+
import { isJsonMode, printJson, exitWithError } from '../utils/output.js';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
function parseEnvFile(file) {
|
|
10
10
|
const out = {};
|
|
@@ -145,14 +145,11 @@ Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<nam
|
|
|
145
145
|
}
|
|
146
146
|
if (options.fromEnvFile) {
|
|
147
147
|
if (!fs.existsSync(options.fromEnvFile)) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.error(msg);
|
|
154
|
-
}
|
|
155
|
-
process.exit(2);
|
|
148
|
+
exitWithError({
|
|
149
|
+
code: 2,
|
|
150
|
+
kind: 'usage',
|
|
151
|
+
message: `--from-env-file: file not found: ${options.fromEnvFile}`,
|
|
152
|
+
});
|
|
156
153
|
}
|
|
157
154
|
const parsed = parseEnvFile(options.fromEnvFile);
|
|
158
155
|
token = token ?? parsed.token;
|
|
@@ -160,37 +157,33 @@ Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<nam
|
|
|
160
157
|
}
|
|
161
158
|
if (options.fromOp) {
|
|
162
159
|
if (!options.opSecret) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
console.error(msg);
|
|
169
|
-
}
|
|
170
|
-
process.exit(2);
|
|
160
|
+
exitWithError({
|
|
161
|
+
code: 2,
|
|
162
|
+
kind: 'usage',
|
|
163
|
+
message: '--from-op requires --op-secret <ref> for the secret reference.',
|
|
164
|
+
});
|
|
171
165
|
}
|
|
172
166
|
try {
|
|
173
167
|
token = readFromOp(options.fromOp);
|
|
174
168
|
secret = readFromOp(options.opSecret);
|
|
175
169
|
}
|
|
176
170
|
catch (err) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.error('Ensure the "op" CLI is installed and authenticated (op signin).');
|
|
184
|
-
}
|
|
185
|
-
process.exit(1);
|
|
171
|
+
exitWithError({
|
|
172
|
+
code: 1,
|
|
173
|
+
kind: 'runtime',
|
|
174
|
+
message: `1Password CLI read failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
175
|
+
hint: 'Ensure the "op" CLI is installed and authenticated (op signin).',
|
|
176
|
+
});
|
|
186
177
|
}
|
|
187
178
|
}
|
|
188
179
|
// No credentials yet and stdin is a TTY → interactive prompt (safest path).
|
|
189
180
|
if ((!token || !secret) && !options.fromEnvFile && !options.fromOp && process.stdin.isTTY) {
|
|
190
181
|
if (isJsonMode()) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
182
|
+
exitWithError({
|
|
183
|
+
code: 2,
|
|
184
|
+
kind: 'usage',
|
|
185
|
+
message: 'Interactive mode cannot run under --json. Provide token/secret via --from-env-file, --from-op, or positional args.',
|
|
186
|
+
});
|
|
194
187
|
}
|
|
195
188
|
try {
|
|
196
189
|
if (!token)
|
|
@@ -204,14 +197,11 @@ Files are written with mode 0600. Profiles live under ~/.switchbot/profiles/<nam
|
|
|
204
197
|
}
|
|
205
198
|
}
|
|
206
199
|
if (!token || !secret) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.error(msg);
|
|
213
|
-
}
|
|
214
|
-
process.exit(2);
|
|
200
|
+
exitWithError({
|
|
201
|
+
code: 2,
|
|
202
|
+
kind: 'usage',
|
|
203
|
+
message: 'Missing token/secret. Run interactively, or use --from-env-file / --from-op, or pass positional arguments (discouraged).',
|
|
204
|
+
});
|
|
215
205
|
}
|
|
216
206
|
saveConfig(token, secret, {
|
|
217
207
|
label: options.label,
|