@switchbot/openapi-cli 2.2.1 → 2.4.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.
- package/README.md +2 -0
- package/dist/api/client.js +33 -1
- package/dist/commands/agent-bootstrap.js +125 -0
- package/dist/commands/batch.js +108 -14
- package/dist/commands/capabilities.js +200 -62
- package/dist/commands/config.js +132 -9
- package/dist/commands/devices.js +85 -12
- package/dist/commands/doctor.js +105 -14
- package/dist/commands/events.js +53 -4
- package/dist/commands/expand.js +1 -77
- package/dist/commands/history.js +124 -2
- package/dist/commands/mcp.js +81 -2
- package/dist/commands/quota.js +4 -2
- package/dist/commands/schema.js +95 -5
- package/dist/config.js +71 -2
- package/dist/devices/history-query.js +181 -0
- package/dist/devices/param-validator.js +263 -0
- package/dist/index.js +12 -2
- package/dist/lib/devices.js +34 -15
- package/dist/lib/idempotency.js +56 -22
- package/dist/mcp/device-history.js +75 -7
- package/dist/utils/arg-parsers.js +4 -1
- package/dist/utils/audit.js +66 -1
- package/dist/utils/flags.js +18 -0
- package/dist/utils/name-resolver.js +76 -28
- package/dist/utils/output.js +115 -19
- package/dist/utils/quota.js +14 -0
- package/dist/utils/redact.js +68 -0
- package/package.json +1 -1
package/dist/utils/output.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Table from 'cli-table3';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { ApiError, DryRunSignal } from '../api/client.js';
|
|
4
|
-
import { getFormat } from './flags.js';
|
|
4
|
+
import { getFormat, getTableStyle } from './flags.js';
|
|
5
5
|
export const SCHEMA_VERSION = '1.1';
|
|
6
6
|
export function isJsonMode() {
|
|
7
7
|
return process.argv.includes('--json') || getFormat() === 'json';
|
|
@@ -9,33 +9,96 @@ export function isJsonMode() {
|
|
|
9
9
|
export function printJson(data) {
|
|
10
10
|
console.log(JSON.stringify({ schemaVersion: SCHEMA_VERSION, data }, null, 2));
|
|
11
11
|
}
|
|
12
|
+
function escapeMarkdownCell(s) {
|
|
13
|
+
// Pipes break markdown table layout; backslash-escape them. Collapse
|
|
14
|
+
// newlines into <br> so each row stays on one line.
|
|
15
|
+
return s.replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
16
|
+
}
|
|
17
|
+
function formatCell(cell, style) {
|
|
18
|
+
if (cell === null || cell === undefined)
|
|
19
|
+
return style === 'markdown' ? '—' : chalk.grey('—');
|
|
20
|
+
if (typeof cell === 'boolean') {
|
|
21
|
+
if (style === 'markdown')
|
|
22
|
+
return cell ? 'Yes' : 'No';
|
|
23
|
+
return cell ? chalk.green('✓') : chalk.red('✗');
|
|
24
|
+
}
|
|
25
|
+
return String(cell);
|
|
26
|
+
}
|
|
27
|
+
function renderMarkdownTable(headers, rows) {
|
|
28
|
+
const head = `| ${headers.map(escapeMarkdownCell).join(' | ')} |`;
|
|
29
|
+
const sep = `| ${headers.map(() => '---').join(' | ')} |`;
|
|
30
|
+
const body = rows.map((r) => `| ${r
|
|
31
|
+
.map((c) => escapeMarkdownCell(formatCell(c, 'markdown')))
|
|
32
|
+
.join(' | ')} |`);
|
|
33
|
+
return [head, sep, ...body].join('\n');
|
|
34
|
+
}
|
|
35
|
+
function renderSimpleTable(headers, rows) {
|
|
36
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(formatCell(r[i], 'simple')).length)));
|
|
37
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
|
|
38
|
+
return [
|
|
39
|
+
fmt(headers),
|
|
40
|
+
...rows.map((r) => fmt(r.map((c) => String(formatCell(c, 'simple'))))),
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
const ASCII_BORDER_CHARS = {
|
|
44
|
+
top: '-', 'top-mid': '+', 'top-left': '+', 'top-right': '+',
|
|
45
|
+
bottom: '-', 'bottom-mid': '+', 'bottom-left': '+', 'bottom-right': '+',
|
|
46
|
+
left: '|', 'left-mid': '+', mid: '-', 'mid-mid': '+',
|
|
47
|
+
right: '|', 'right-mid': '+', middle: '|',
|
|
48
|
+
};
|
|
12
49
|
export function printTable(headers, rows) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
50
|
+
const style = getTableStyle();
|
|
51
|
+
if (style === 'markdown') {
|
|
52
|
+
console.log(renderMarkdownTable(headers, rows));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (style === 'simple') {
|
|
56
|
+
console.log(renderSimpleTable(headers, rows));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const tableOpts = {
|
|
60
|
+
head: headers.map((h) => (style === 'ascii' ? h : chalk.cyan(h))),
|
|
61
|
+
style: style === 'ascii' ? { border: [], head: [] } : { border: ['grey'] },
|
|
62
|
+
};
|
|
63
|
+
if (style === 'ascii') {
|
|
64
|
+
tableOpts.chars = ASCII_BORDER_CHARS;
|
|
65
|
+
}
|
|
66
|
+
const table = new Table(tableOpts);
|
|
17
67
|
for (const row of rows) {
|
|
18
|
-
table.push(row.map((cell) =>
|
|
19
|
-
if (cell === null || cell === undefined)
|
|
20
|
-
return chalk.grey('—');
|
|
21
|
-
if (typeof cell === 'boolean')
|
|
22
|
-
return cell ? chalk.green('✓') : chalk.red('✗');
|
|
23
|
-
return String(cell);
|
|
24
|
-
}));
|
|
68
|
+
table.push(row.map((cell) => formatCell(cell, style)));
|
|
25
69
|
}
|
|
26
70
|
console.log(table.toString());
|
|
27
71
|
}
|
|
28
72
|
export function printKeyValue(data) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
73
|
+
const style = getTableStyle();
|
|
74
|
+
if (style === 'markdown') {
|
|
75
|
+
const entries = Object.entries(data).filter(([, v]) => v !== null && v !== undefined);
|
|
76
|
+
const rows = entries.map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]);
|
|
77
|
+
console.log(renderMarkdownTable(['Key', 'Value'], rows));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (style === 'simple') {
|
|
81
|
+
for (const [key, value] of Object.entries(data)) {
|
|
82
|
+
if (value === null || value === undefined)
|
|
83
|
+
continue;
|
|
84
|
+
const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
85
|
+
console.log(`${key} ${displayValue}`);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const tableOpts = {
|
|
90
|
+
style: style === 'ascii' ? { border: [], head: [] } : { border: ['grey'] },
|
|
91
|
+
};
|
|
92
|
+
if (style === 'ascii') {
|
|
93
|
+
tableOpts.chars = ASCII_BORDER_CHARS;
|
|
94
|
+
}
|
|
95
|
+
const table = new Table(tableOpts);
|
|
32
96
|
for (const [key, value] of Object.entries(data)) {
|
|
33
97
|
if (value === null || value === undefined)
|
|
34
98
|
continue;
|
|
35
|
-
const displayValue = typeof value === 'object'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
table.push({ [chalk.cyan(key)]: displayValue });
|
|
99
|
+
const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
100
|
+
const keyLabel = style === 'ascii' ? key : chalk.cyan(key);
|
|
101
|
+
table.push({ [keyLabel]: displayValue });
|
|
39
102
|
}
|
|
40
103
|
console.log(table.toString());
|
|
41
104
|
}
|
|
@@ -82,6 +145,35 @@ export function buildErrorPayload(error) {
|
|
|
82
145
|
if (error instanceof UsageError) {
|
|
83
146
|
return { code: 2, kind: 'usage', message: error.message, errorClass: 'usage', transient: false };
|
|
84
147
|
}
|
|
148
|
+
// Idempotency conflict → exit 2 with kind:guard so scripts can react.
|
|
149
|
+
if (error instanceof Error && error.name === 'IdempotencyConflictError') {
|
|
150
|
+
return {
|
|
151
|
+
code: 2,
|
|
152
|
+
kind: 'guard',
|
|
153
|
+
message: error.message,
|
|
154
|
+
errorClass: 'guard',
|
|
155
|
+
transient: false,
|
|
156
|
+
context: {
|
|
157
|
+
existingShape: error.existingShape,
|
|
158
|
+
newShape: error.newShape,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Local daily-cap refusal → exit 2 (usage-style refusal before touching net).
|
|
163
|
+
if (error instanceof Error && error.name === 'DailyCapExceededError') {
|
|
164
|
+
return {
|
|
165
|
+
code: 2,
|
|
166
|
+
kind: 'guard',
|
|
167
|
+
message: error.message,
|
|
168
|
+
errorClass: 'guard',
|
|
169
|
+
transient: false,
|
|
170
|
+
context: {
|
|
171
|
+
cap: error.cap,
|
|
172
|
+
total: error.total,
|
|
173
|
+
profile: error.profile,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
85
177
|
const code = error instanceof ApiError ? error.code : 1;
|
|
86
178
|
const kind = error instanceof ApiError ? 'api' : 'runtime';
|
|
87
179
|
const message = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
@@ -127,6 +219,10 @@ export function handleError(error) {
|
|
|
127
219
|
console.error(payload.message);
|
|
128
220
|
process.exit(2);
|
|
129
221
|
}
|
|
222
|
+
if (payload.kind === 'guard') {
|
|
223
|
+
console.error(chalk.yellow(`Guard: ${payload.message}`));
|
|
224
|
+
process.exit(payload.code === 2 ? 2 : 1);
|
|
225
|
+
}
|
|
130
226
|
if (error instanceof ApiError) {
|
|
131
227
|
console.error(chalk.red(`Error (code ${error.code}): ${payload.message}`));
|
|
132
228
|
if (payload.hint)
|
package/dist/utils/quota.js
CHANGED
|
@@ -211,3 +211,17 @@ export function todayUsage(now = new Date()) {
|
|
|
211
211
|
endpoints: { ...bucket.endpoints },
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Check whether today's call count is at or over the given cap. Returns the
|
|
216
|
+
* current counter either way so callers can render a helpful refusal message.
|
|
217
|
+
* Undefined cap → returns { over: false } without loading anything.
|
|
218
|
+
*/
|
|
219
|
+
export function checkDailyCap(dailyCap, now = new Date()) {
|
|
220
|
+
const date = today(now);
|
|
221
|
+
if (!dailyCap || dailyCap <= 0) {
|
|
222
|
+
return { over: false, total: 0, date };
|
|
223
|
+
}
|
|
224
|
+
const data = loadQuota();
|
|
225
|
+
const total = data.days[date]?.total ?? 0;
|
|
226
|
+
return { over: total >= dailyCap, total, cap: dailyCap, date };
|
|
227
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header/value redaction utilities for verbose traces.
|
|
3
|
+
*
|
|
4
|
+
* C6 contract: any header whose name matches a sensitive pattern is mid-masked
|
|
5
|
+
* (first 2 chars + `*` run + last 2 chars) before it is written to stderr. The
|
|
6
|
+
* `--trace-unsafe` flag turns masking off — with a prominent one-time warning.
|
|
7
|
+
*/
|
|
8
|
+
import { isTraceUnsafe } from './flags.js';
|
|
9
|
+
const SENSITIVE_HEADER_PATTERNS = [
|
|
10
|
+
/^authorization$/i,
|
|
11
|
+
/^token$/i,
|
|
12
|
+
/^sign$/i,
|
|
13
|
+
/^nonce$/i,
|
|
14
|
+
/^x-api-key$/i,
|
|
15
|
+
/^cookie$/i,
|
|
16
|
+
/^set-cookie$/i,
|
|
17
|
+
/^x-auth-token$/i,
|
|
18
|
+
];
|
|
19
|
+
// The `t` header (timestamp) is treated as sensitive alongside sign because
|
|
20
|
+
// together they reconstruct the HMAC signature — anyone watching the logs
|
|
21
|
+
// shouldn't be able to replay the exact timestamp that was used.
|
|
22
|
+
const SENSITIVE_EXACT_KEYS = new Set(['t']);
|
|
23
|
+
export function isSensitiveHeader(name) {
|
|
24
|
+
if (SENSITIVE_EXACT_KEYS.has(name))
|
|
25
|
+
return true;
|
|
26
|
+
return SENSITIVE_HEADER_PATTERNS.some((re) => re.test(name));
|
|
27
|
+
}
|
|
28
|
+
export function maskValue(value) {
|
|
29
|
+
if (value.length <= 4)
|
|
30
|
+
return '****';
|
|
31
|
+
return `${value.slice(0, 2)}${'*'.repeat(Math.max(4, value.length - 4))}${value.slice(-2)}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Redact the sensitive entries of a headers object. Returns a new object
|
|
35
|
+
* alongside the count of entries that were masked.
|
|
36
|
+
*/
|
|
37
|
+
export function redactHeaders(headers) {
|
|
38
|
+
const safe = {};
|
|
39
|
+
let redactedCount = 0;
|
|
40
|
+
if (!headers)
|
|
41
|
+
return { safe, redactedCount };
|
|
42
|
+
const unsafe = isTraceUnsafe();
|
|
43
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
44
|
+
const strVal = typeof v === 'string' ? v : v == null ? '' : String(v);
|
|
45
|
+
if (!unsafe && isSensitiveHeader(k)) {
|
|
46
|
+
safe[k] = maskValue(strVal);
|
|
47
|
+
redactedCount++;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
safe[k] = strVal;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { safe, redactedCount };
|
|
54
|
+
}
|
|
55
|
+
let unsafeBannerShown = false;
|
|
56
|
+
/**
|
|
57
|
+
* Print the big "REDACTION DISABLED" banner once per process when
|
|
58
|
+
* --trace-unsafe is on. Callers should invoke this once before any
|
|
59
|
+
* header-spilling output.
|
|
60
|
+
*/
|
|
61
|
+
export function warnOnceIfUnsafe() {
|
|
62
|
+
if (unsafeBannerShown)
|
|
63
|
+
return;
|
|
64
|
+
if (!isTraceUnsafe())
|
|
65
|
+
return;
|
|
66
|
+
unsafeBannerShown = true;
|
|
67
|
+
process.stderr.write('⚠️ --trace-unsafe: sensitive headers will be printed UNMASKED. Do not share this output.\n');
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchbot/openapi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"switchbot",
|