@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.
- package/README.md +3 -3
- package/dist/index.js +56945 -169
- package/dist/policy/schema/v0.2.json +1 -1
- package/package.json +3 -2
- package/dist/api/client.js +0 -235
- package/dist/auth.js +0 -20
- package/dist/commands/agent-bootstrap.js +0 -182
- package/dist/commands/auth.js +0 -354
- package/dist/commands/batch.js +0 -413
- package/dist/commands/cache.js +0 -126
- package/dist/commands/capabilities.js +0 -385
- package/dist/commands/catalog.js +0 -359
- package/dist/commands/completion.js +0 -385
- package/dist/commands/config.js +0 -376
- package/dist/commands/daemon.js +0 -410
- package/dist/commands/device-meta.js +0 -159
- package/dist/commands/devices.js +0 -948
- package/dist/commands/doctor.js +0 -1015
- package/dist/commands/events.js +0 -563
- package/dist/commands/expand.js +0 -130
- package/dist/commands/explain.js +0 -139
- package/dist/commands/health.js +0 -113
- package/dist/commands/history.js +0 -320
- package/dist/commands/identity.js +0 -59
- package/dist/commands/install.js +0 -246
- package/dist/commands/mcp.js +0 -2017
- package/dist/commands/plan.js +0 -653
- package/dist/commands/policy.js +0 -586
- package/dist/commands/quota.js +0 -78
- package/dist/commands/rules.js +0 -875
- package/dist/commands/scenes.js +0 -264
- package/dist/commands/schema.js +0 -177
- package/dist/commands/status-sync.js +0 -131
- package/dist/commands/uninstall.js +0 -237
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/watch.js +0 -194
- package/dist/commands/webhook.js +0 -182
- package/dist/config.js +0 -258
- package/dist/credentials/backends/file.js +0 -101
- package/dist/credentials/backends/linux.js +0 -129
- package/dist/credentials/backends/macos.js +0 -129
- package/dist/credentials/backends/windows.js +0 -215
- package/dist/credentials/keychain.js +0 -88
- package/dist/credentials/prime.js +0 -52
- package/dist/devices/cache.js +0 -293
- package/dist/devices/catalog.js +0 -767
- package/dist/devices/device-meta.js +0 -56
- package/dist/devices/history-agg.js +0 -138
- package/dist/devices/history-query.js +0 -181
- package/dist/devices/param-validator.js +0 -433
- package/dist/devices/resources.js +0 -270
- package/dist/install/default-steps.js +0 -257
- package/dist/install/preflight.js +0 -212
- package/dist/install/steps.js +0 -67
- package/dist/lib/command-keywords.js +0 -17
- package/dist/lib/daemon-state.js +0 -46
- package/dist/lib/destructive-mode.js +0 -12
- package/dist/lib/devices.js +0 -382
- package/dist/lib/idempotency.js +0 -106
- package/dist/lib/plan-store.js +0 -68
- package/dist/lib/request-context.js +0 -12
- package/dist/lib/scenes.js +0 -10
- package/dist/logger.js +0 -16
- package/dist/mcp/device-history.js +0 -145
- package/dist/mcp/events-subscription.js +0 -213
- package/dist/mqtt/client.js +0 -180
- package/dist/mqtt/credential.js +0 -30
- package/dist/policy/add-rule.js +0 -124
- package/dist/policy/diff.js +0 -91
- package/dist/policy/format.js +0 -57
- package/dist/policy/load.js +0 -61
- package/dist/policy/migrate.js +0 -67
- package/dist/policy/schema.js +0 -18
- package/dist/policy/validate.js +0 -262
- package/dist/rules/action.js +0 -216
- package/dist/rules/audit-query.js +0 -89
- package/dist/rules/conflict-analyzer.js +0 -214
- package/dist/rules/cron-scheduler.js +0 -186
- package/dist/rules/destructive.js +0 -52
- package/dist/rules/engine.js +0 -757
- package/dist/rules/matcher.js +0 -230
- package/dist/rules/pid-file.js +0 -95
- package/dist/rules/quiet-hours.js +0 -45
- package/dist/rules/suggest.js +0 -95
- package/dist/rules/throttle.js +0 -116
- package/dist/rules/types.js +0 -34
- package/dist/rules/webhook-listener.js +0 -223
- package/dist/rules/webhook-token.js +0 -90
- package/dist/schema/field-aliases.js +0 -131
- package/dist/sinks/dispatcher.js +0 -12
- package/dist/sinks/file.js +0 -19
- package/dist/sinks/format.js +0 -56
- package/dist/sinks/homeassistant.js +0 -44
- package/dist/sinks/openclaw.js +0 -33
- package/dist/sinks/stdout.js +0 -5
- package/dist/sinks/telegram.js +0 -28
- package/dist/sinks/types.js +0 -1
- package/dist/sinks/webhook.js +0 -22
- package/dist/status-sync/manager.js +0 -268
- package/dist/utils/arg-parsers.js +0 -66
- package/dist/utils/audit.js +0 -121
- package/dist/utils/filter.js +0 -189
- package/dist/utils/flags.js +0 -186
- package/dist/utils/format.js +0 -117
- package/dist/utils/health.js +0 -101
- package/dist/utils/help-json.js +0 -54
- package/dist/utils/name-resolver.js +0 -137
- package/dist/utils/output.js +0 -404
- package/dist/utils/quota.js +0 -227
- package/dist/utils/redact.js +0 -68
- package/dist/utils/retry.js +0 -140
- package/dist/utils/string.js +0 -22
- package/dist/version.js +0 -4
package/dist/policy/diff.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
export const MAX_POLICY_DIFF_CHANGES = 200;
|
|
2
|
-
function isPlainObject(v) {
|
|
3
|
-
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
|
-
}
|
|
5
|
-
function collectPolicyDiff(left, right, at, out, limit) {
|
|
6
|
-
if (out.length >= limit)
|
|
7
|
-
return;
|
|
8
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
9
|
-
const maxLen = Math.max(left.length, right.length);
|
|
10
|
-
for (let i = 0; i < maxLen; i++) {
|
|
11
|
-
if (out.length >= limit)
|
|
12
|
-
return;
|
|
13
|
-
const path = `${at}[${i}]`;
|
|
14
|
-
if (i >= left.length) {
|
|
15
|
-
out.push({ path, kind: 'added', after: right[i] });
|
|
16
|
-
}
|
|
17
|
-
else if (i >= right.length) {
|
|
18
|
-
out.push({ path, kind: 'removed', before: left[i] });
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
collectPolicyDiff(left[i], right[i], path, out, limit);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (isPlainObject(left) && isPlainObject(right)) {
|
|
27
|
-
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
28
|
-
for (const key of [...keys].sort()) {
|
|
29
|
-
if (out.length >= limit)
|
|
30
|
-
return;
|
|
31
|
-
const path = at === '$' ? `$.${key}` : `${at}.${key}`;
|
|
32
|
-
const leftHas = Object.prototype.hasOwnProperty.call(left, key);
|
|
33
|
-
const rightHas = Object.prototype.hasOwnProperty.call(right, key);
|
|
34
|
-
if (!leftHas && rightHas) {
|
|
35
|
-
out.push({ path, kind: 'added', after: right[key] });
|
|
36
|
-
}
|
|
37
|
-
else if (leftHas && !rightHas) {
|
|
38
|
-
out.push({ path, kind: 'removed', before: left[key] });
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
collectPolicyDiff(left[key], right[key], path, out, limit);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (!Object.is(left, right)) {
|
|
47
|
-
out.push({ path: at, kind: 'changed', before: left, after: right });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function buildLineDiff(before, after) {
|
|
51
|
-
const beforeLines = before.split('\n');
|
|
52
|
-
const afterLines = after.split('\n');
|
|
53
|
-
const lines = ['--- before', '+++ after'];
|
|
54
|
-
let i = 0;
|
|
55
|
-
let j = 0;
|
|
56
|
-
while (i < beforeLines.length || j < afterLines.length) {
|
|
57
|
-
const b = beforeLines[i];
|
|
58
|
-
const a = afterLines[j];
|
|
59
|
-
if (i < beforeLines.length && j < afterLines.length && b === a) {
|
|
60
|
-
lines.push(` ${b}`);
|
|
61
|
-
i++;
|
|
62
|
-
j++;
|
|
63
|
-
}
|
|
64
|
-
else if (j < afterLines.length && (i >= beforeLines.length || b !== a)) {
|
|
65
|
-
lines.push(`+${a}`);
|
|
66
|
-
j++;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
lines.push(`-${b}`);
|
|
70
|
-
i++;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return lines.join('\n');
|
|
74
|
-
}
|
|
75
|
-
export function diffPolicyValues(leftDoc, rightDoc, leftSource, rightSource, maxChanges = MAX_POLICY_DIFF_CHANGES) {
|
|
76
|
-
const changes = [];
|
|
77
|
-
collectPolicyDiff(leftDoc, rightDoc, '$', changes, maxChanges);
|
|
78
|
-
const equal = changes.length === 0;
|
|
79
|
-
return {
|
|
80
|
-
equal,
|
|
81
|
-
changeCount: changes.length,
|
|
82
|
-
truncated: changes.length >= maxChanges,
|
|
83
|
-
stats: {
|
|
84
|
-
added: changes.filter((c) => c.kind === 'added').length,
|
|
85
|
-
removed: changes.filter((c) => c.kind === 'removed').length,
|
|
86
|
-
changed: changes.filter((c) => c.kind === 'changed').length,
|
|
87
|
-
},
|
|
88
|
-
changes,
|
|
89
|
-
diff: buildLineDiff(leftSource, rightSource),
|
|
90
|
-
};
|
|
91
|
-
}
|
package/dist/policy/format.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import chalk, { Chalk } from 'chalk';
|
|
2
|
-
const noColorChalk = new Chalk({ level: 0 });
|
|
3
|
-
function colorize(enabled) {
|
|
4
|
-
return enabled ? chalk : noColorChalk;
|
|
5
|
-
}
|
|
6
|
-
function snippet(source, line, col, length, c) {
|
|
7
|
-
const lines = source.split(/\r?\n/);
|
|
8
|
-
if (line < 1 || line > lines.length)
|
|
9
|
-
return '';
|
|
10
|
-
const lineText = lines[line - 1];
|
|
11
|
-
const gutter = ` ${line} | `;
|
|
12
|
-
const pad = ' '.repeat(gutter.length);
|
|
13
|
-
const caretStart = Math.max(0, col - 1);
|
|
14
|
-
const caretLen = Math.max(1, length);
|
|
15
|
-
const caret = `${' '.repeat(caretStart)}${c.red('^'.repeat(caretLen))}`;
|
|
16
|
-
return `${c.dim(gutter)}${lineText}\n${c.dim(pad)}${caret}`;
|
|
17
|
-
}
|
|
18
|
-
function estimateTokenLength(source, line, col) {
|
|
19
|
-
const lines = source.split(/\r?\n/);
|
|
20
|
-
if (line < 1 || line > lines.length)
|
|
21
|
-
return 1;
|
|
22
|
-
const lineText = lines[line - 1];
|
|
23
|
-
const start = Math.max(0, col - 1);
|
|
24
|
-
if (start >= lineText.length)
|
|
25
|
-
return 1;
|
|
26
|
-
const rest = lineText.slice(start);
|
|
27
|
-
const quoted = rest.match(/^(['"]).*?\1/);
|
|
28
|
-
if (quoted)
|
|
29
|
-
return quoted[0].length;
|
|
30
|
-
const token = rest.match(/^[^\s,\[\]{}]+/);
|
|
31
|
-
return token ? token[0].length : 1;
|
|
32
|
-
}
|
|
33
|
-
function formatError(err, policyPath, source, opts) {
|
|
34
|
-
const c = colorize(opts.color ?? true);
|
|
35
|
-
const loc = err.line !== undefined && err.col !== undefined ? `${err.line}:${err.col}` : '(unknown)';
|
|
36
|
-
const header = `${c.cyan(policyPath)}:${c.yellow(loc)}`;
|
|
37
|
-
const body = [`${c.red.bold('error')}: ${err.message}`];
|
|
38
|
-
if (err.line !== undefined && err.col !== undefined && !opts.noSnippet) {
|
|
39
|
-
const len = estimateTokenLength(source, err.line, err.col);
|
|
40
|
-
const snip = snippet(source, err.line, err.col, len, c);
|
|
41
|
-
if (snip)
|
|
42
|
-
body.unshift(snip);
|
|
43
|
-
}
|
|
44
|
-
if (err.hint)
|
|
45
|
-
body.push(`${c.green.bold('hint')}: ${err.hint}`);
|
|
46
|
-
return [header, ...body].join('\n');
|
|
47
|
-
}
|
|
48
|
-
export function formatValidationResult(result, source, opts = {}) {
|
|
49
|
-
const c = colorize(opts.color ?? true);
|
|
50
|
-
if (result.valid) {
|
|
51
|
-
return `${c.green.bold('✓')} ${result.policyPath} is valid (schema v${result.schemaVersion})`;
|
|
52
|
-
}
|
|
53
|
-
const blocks = result.errors.map((e) => formatError(e, result.policyPath, source, opts));
|
|
54
|
-
const count = result.errors.length;
|
|
55
|
-
const footer = `${c.red.bold(`✗ ${count} ${count === 1 ? 'error' : 'errors'}`)} in ${result.policyPath} (schema v${result.schemaVersion})`;
|
|
56
|
-
return [...blocks, '', footer].join('\n\n').replace(/\n{3,}/g, '\n\n');
|
|
57
|
-
}
|
package/dist/policy/load.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { parseDocument, LineCounter } from 'yaml';
|
|
5
|
-
export const DEFAULT_POLICY_PATH = join(homedir(), '.config', 'openclaw', 'switchbot', 'policy.yaml');
|
|
6
|
-
export function resolvePolicyPath(options = {}) {
|
|
7
|
-
const { flag, env = process.env } = options;
|
|
8
|
-
if (flag && flag.trim().length > 0)
|
|
9
|
-
return resolve(flag);
|
|
10
|
-
const fromEnv = env.SWITCHBOT_POLICY_PATH;
|
|
11
|
-
if (fromEnv && fromEnv.trim().length > 0)
|
|
12
|
-
return resolve(fromEnv);
|
|
13
|
-
return DEFAULT_POLICY_PATH;
|
|
14
|
-
}
|
|
15
|
-
export class PolicyFileNotFoundError extends Error {
|
|
16
|
-
policyPath;
|
|
17
|
-
constructor(policyPath) {
|
|
18
|
-
super(`policy file not found: ${policyPath}`);
|
|
19
|
-
this.policyPath = policyPath;
|
|
20
|
-
this.name = 'PolicyFileNotFoundError';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export class PolicyYamlParseError extends Error {
|
|
24
|
-
policyPath;
|
|
25
|
-
yamlErrors;
|
|
26
|
-
constructor(message, policyPath, yamlErrors) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.policyPath = policyPath;
|
|
29
|
-
this.yamlErrors = yamlErrors;
|
|
30
|
-
this.name = 'PolicyYamlParseError';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export function loadPolicyFile(policyPath) {
|
|
34
|
-
let source;
|
|
35
|
-
try {
|
|
36
|
-
source = readFileSync(policyPath, 'utf-8');
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
const e = err;
|
|
40
|
-
if (e.code === 'ENOENT')
|
|
41
|
-
throw new PolicyFileNotFoundError(policyPath);
|
|
42
|
-
throw err;
|
|
43
|
-
}
|
|
44
|
-
const lineCounter = new LineCounter();
|
|
45
|
-
const doc = parseDocument(source, { lineCounter, keepSourceTokens: true });
|
|
46
|
-
if (doc.errors.length > 0) {
|
|
47
|
-
const yamlErrors = doc.errors.map((e) => {
|
|
48
|
-
const pos = e.pos?.[0];
|
|
49
|
-
const loc = pos !== undefined ? lineCounter.linePos(pos) : undefined;
|
|
50
|
-
return { line: loc?.line, col: loc?.col, message: e.message };
|
|
51
|
-
});
|
|
52
|
-
throw new PolicyYamlParseError(doc.errors[0].message, policyPath, yamlErrors);
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
path: policyPath,
|
|
56
|
-
source,
|
|
57
|
-
doc,
|
|
58
|
-
lineCounter,
|
|
59
|
-
data: doc.toJS({ maxAliasCount: 100 }),
|
|
60
|
-
};
|
|
61
|
-
}
|
package/dist/policy/migrate.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { isMap, isScalar, parseDocument, LineCounter } from 'yaml';
|
|
2
|
-
import { validateLoadedPolicy } from './validate.js';
|
|
3
|
-
export class PolicyMigrationError extends Error {
|
|
4
|
-
code;
|
|
5
|
-
constructor(message, code) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.code = code;
|
|
8
|
-
this.name = 'PolicyMigrationError';
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
const MIGRATION_CHAIN = [];
|
|
12
|
-
function bumpVersionScalar(doc, target) {
|
|
13
|
-
if (!isMap(doc.contents)) {
|
|
14
|
-
throw new PolicyMigrationError('policy root must be a YAML mapping (got null or an array)', 'invalid-shape');
|
|
15
|
-
}
|
|
16
|
-
const pair = doc.contents.items.find((p) => isScalar(p.key) && p.key.value === 'version');
|
|
17
|
-
if (!pair || !isScalar(pair.value)) {
|
|
18
|
-
throw new PolicyMigrationError('policy has no `version` scalar to migrate; add `version: "0.2"` (or `"0.1"`) and retry', 'no-version-field');
|
|
19
|
-
}
|
|
20
|
-
pair.value.value = target;
|
|
21
|
-
}
|
|
22
|
-
function findPlan(from, to) {
|
|
23
|
-
const chain = [];
|
|
24
|
-
let cur = from;
|
|
25
|
-
while (cur !== to) {
|
|
26
|
-
const step = MIGRATION_CHAIN.find((p) => p.fromVersion === cur);
|
|
27
|
-
if (!step) {
|
|
28
|
-
throw new PolicyMigrationError(`no migration path from v${from} to v${to} (missing step at v${cur})`, 'no-path');
|
|
29
|
-
}
|
|
30
|
-
chain.push(step);
|
|
31
|
-
cur = step.toVersion;
|
|
32
|
-
}
|
|
33
|
-
return chain;
|
|
34
|
-
}
|
|
35
|
-
export function planMigration(loaded, from, to) {
|
|
36
|
-
if (from === to) {
|
|
37
|
-
const precheck = validateLoadedPolicy(loaded);
|
|
38
|
-
return { changed: false, fromVersion: from, toVersion: to, nextSource: loaded.source, precheck };
|
|
39
|
-
}
|
|
40
|
-
const plan = findPlan(from, to);
|
|
41
|
-
// Round-trip through source instead of Document.clone(): keeps comments +
|
|
42
|
-
// anchors intact, works across yaml library versions, and leaves the
|
|
43
|
-
// caller's `loaded.doc` untouched.
|
|
44
|
-
const nextLineCounter = new LineCounter();
|
|
45
|
-
const clone = parseDocument(loaded.source, {
|
|
46
|
-
lineCounter: nextLineCounter,
|
|
47
|
-
keepSourceTokens: true,
|
|
48
|
-
});
|
|
49
|
-
for (const step of plan)
|
|
50
|
-
step.migrate(clone);
|
|
51
|
-
const nextSource = String(clone);
|
|
52
|
-
// Re-parse after serialization so `doc` and `source` stay in sync for the
|
|
53
|
-
// validator's line/col mapping.
|
|
54
|
-
const reLineCounter = new LineCounter();
|
|
55
|
-
const reDoc = parseDocument(nextSource, {
|
|
56
|
-
lineCounter: reLineCounter,
|
|
57
|
-
keepSourceTokens: true,
|
|
58
|
-
});
|
|
59
|
-
const precheck = validateLoadedPolicy({
|
|
60
|
-
path: loaded.path,
|
|
61
|
-
source: nextSource,
|
|
62
|
-
doc: reDoc,
|
|
63
|
-
lineCounter: reLineCounter,
|
|
64
|
-
data: reDoc.toJS({ maxAliasCount: 100 }),
|
|
65
|
-
});
|
|
66
|
-
return { changed: true, fromVersion: from, toVersion: to, nextSource, precheck };
|
|
67
|
-
}
|
package/dist/policy/schema.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
export const SUPPORTED_POLICY_SCHEMA_VERSIONS = ['0.2'];
|
|
4
|
-
export const CURRENT_POLICY_SCHEMA_VERSION = '0.2';
|
|
5
|
-
const schemaCache = new Map();
|
|
6
|
-
export function loadPolicySchema(version = CURRENT_POLICY_SCHEMA_VERSION) {
|
|
7
|
-
const cached = schemaCache.get(version);
|
|
8
|
-
if (cached)
|
|
9
|
-
return cached;
|
|
10
|
-
const url = new URL(`./schema/v${version}.json`, import.meta.url);
|
|
11
|
-
const raw = readFileSync(fileURLToPath(url), 'utf-8');
|
|
12
|
-
const parsed = JSON.parse(raw);
|
|
13
|
-
schemaCache.set(version, parsed);
|
|
14
|
-
return parsed;
|
|
15
|
-
}
|
|
16
|
-
export function isSupportedPolicySchemaVersion(v) {
|
|
17
|
-
return typeof v === 'string' && SUPPORTED_POLICY_SCHEMA_VERSIONS.includes(v);
|
|
18
|
-
}
|
package/dist/policy/validate.js
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
|
-
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
|
-
import { isMap, isSeq, isScalar } from 'yaml';
|
|
4
|
-
import { loadPolicyFile } from './load.js';
|
|
5
|
-
import { loadPolicySchema, CURRENT_POLICY_SCHEMA_VERSION, SUPPORTED_POLICY_SCHEMA_VERSIONS, isSupportedPolicySchemaVersion, } from './schema.js';
|
|
6
|
-
import { destructiveVerbOf, DESTRUCTIVE_COMMANDS } from '../rules/destructive.js';
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const addFormats = require('ajv-formats');
|
|
9
|
-
const validators = new Map();
|
|
10
|
-
function getValidator(version) {
|
|
11
|
-
const cached = validators.get(version);
|
|
12
|
-
if (cached)
|
|
13
|
-
return cached;
|
|
14
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false, allowUnionTypes: true });
|
|
15
|
-
addFormats(ajv);
|
|
16
|
-
const schema = loadPolicySchema(version);
|
|
17
|
-
const validate = ajv.compile(schema);
|
|
18
|
-
const compiled = { ajv, validate };
|
|
19
|
-
validators.set(version, compiled);
|
|
20
|
-
return compiled;
|
|
21
|
-
}
|
|
22
|
-
function instancePathToSegments(instancePath) {
|
|
23
|
-
if (!instancePath)
|
|
24
|
-
return [];
|
|
25
|
-
return instancePath
|
|
26
|
-
.slice(1)
|
|
27
|
-
.split('/')
|
|
28
|
-
.map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
29
|
-
}
|
|
30
|
-
function getNodeAt(doc, segments) {
|
|
31
|
-
let current = doc.contents;
|
|
32
|
-
for (const seg of segments) {
|
|
33
|
-
if (isMap(current)) {
|
|
34
|
-
const pair = current.items.find((p) => {
|
|
35
|
-
const k = p.key;
|
|
36
|
-
if (isScalar(k))
|
|
37
|
-
return String(k.value) === seg;
|
|
38
|
-
return false;
|
|
39
|
-
});
|
|
40
|
-
if (!pair)
|
|
41
|
-
return null;
|
|
42
|
-
current = pair.value;
|
|
43
|
-
}
|
|
44
|
-
else if (isSeq(current)) {
|
|
45
|
-
const idx = Number(seg);
|
|
46
|
-
if (!Number.isInteger(idx))
|
|
47
|
-
return null;
|
|
48
|
-
current = current.items[idx];
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return current ?? null;
|
|
55
|
-
}
|
|
56
|
-
function getKeyNodeAt(doc, parentSegments, key) {
|
|
57
|
-
const parent = parentSegments.length === 0 ? doc.contents : getNodeAt(doc, parentSegments);
|
|
58
|
-
if (!parent || !isMap(parent))
|
|
59
|
-
return null;
|
|
60
|
-
const pair = parent.items.find((p) => isScalar(p.key) && String(p.key.value) === key);
|
|
61
|
-
return pair?.key ?? null;
|
|
62
|
-
}
|
|
63
|
-
function locateError(doc, lineCounter, err) {
|
|
64
|
-
const segments = instancePathToSegments(err.instancePath);
|
|
65
|
-
if (err.keyword === 'additionalProperties') {
|
|
66
|
-
const bad = err.params.additionalProperty;
|
|
67
|
-
if (bad) {
|
|
68
|
-
const keyNode = getKeyNodeAt(doc, segments, bad);
|
|
69
|
-
const range = keyNode?.range;
|
|
70
|
-
if (range) {
|
|
71
|
-
const pos = lineCounter.linePos(range[0]);
|
|
72
|
-
return { line: pos.line, col: pos.col };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (err.keyword === 'required' || err.keyword === 'dependentRequired') {
|
|
77
|
-
const node = getNodeAt(doc, segments);
|
|
78
|
-
const range = node?.range;
|
|
79
|
-
if (range) {
|
|
80
|
-
const pos = lineCounter.linePos(range[0]);
|
|
81
|
-
return { line: pos.line, col: pos.col };
|
|
82
|
-
}
|
|
83
|
-
return { line: 1, col: 1 };
|
|
84
|
-
}
|
|
85
|
-
const node = getNodeAt(doc, segments);
|
|
86
|
-
const range = node?.range;
|
|
87
|
-
if (!range)
|
|
88
|
-
return {};
|
|
89
|
-
const pos = lineCounter.linePos(range[0]);
|
|
90
|
-
return { line: pos.line, col: pos.col };
|
|
91
|
-
}
|
|
92
|
-
function humanMessage(err) {
|
|
93
|
-
const path = err.instancePath || '(root)';
|
|
94
|
-
switch (err.keyword) {
|
|
95
|
-
case 'required':
|
|
96
|
-
return `missing required property "${err.params.missingProperty}"`;
|
|
97
|
-
case 'additionalProperties':
|
|
98
|
-
return `unknown property "${err.params.additionalProperty}"`;
|
|
99
|
-
case 'dependentRequired': {
|
|
100
|
-
const { property, missingProperty } = err.params;
|
|
101
|
-
const parent = path === '(root)' ? '' : `${path}: `;
|
|
102
|
-
return `${parent}when "${property}" is set, "${missingProperty}" is also required`;
|
|
103
|
-
}
|
|
104
|
-
case 'pattern':
|
|
105
|
-
return `${path} does not match pattern ${err.params.pattern}`;
|
|
106
|
-
case 'const':
|
|
107
|
-
return `${path} must be exactly ${JSON.stringify(err.params.allowedValue)}`;
|
|
108
|
-
case 'enum':
|
|
109
|
-
return `${path} must be one of ${JSON.stringify(err.params.allowedValues)}`;
|
|
110
|
-
case 'type':
|
|
111
|
-
return `${path} must be ${err.params.type}`;
|
|
112
|
-
case 'not':
|
|
113
|
-
return `${path} is not allowed here`;
|
|
114
|
-
default:
|
|
115
|
-
return `${path} ${err.message ?? 'is invalid'}`;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function hintFor(err) {
|
|
119
|
-
if (err.keyword === 'pattern' && err.instancePath.startsWith('/aliases/')) {
|
|
120
|
-
return 'paste the deviceId from `switchbot devices list --format=tsv`, e.g. 01-202407090924-26354212';
|
|
121
|
-
}
|
|
122
|
-
if (err.keyword === 'not' && err.instancePath.startsWith('/confirmations/never_confirm/')) {
|
|
123
|
-
return 'destructive actions (lock/unlock/delete*/factoryReset) cannot be pre-approved in policy.yaml';
|
|
124
|
-
}
|
|
125
|
-
if (err.keyword === 'const' && err.instancePath === '/version') {
|
|
126
|
-
const supported = SUPPORTED_POLICY_SCHEMA_VERSIONS.map((v) => `"${v}"`).join(' / ');
|
|
127
|
-
return `this CLI supports policy schema versions ${supported}; run \`switchbot policy migrate\` to upgrade an older file`;
|
|
128
|
-
}
|
|
129
|
-
if (err.keyword === 'required' && err.instancePath === '') {
|
|
130
|
-
const missing = err.params.missingProperty;
|
|
131
|
-
if (missing === 'version')
|
|
132
|
-
return `add \`version: "${CURRENT_POLICY_SCHEMA_VERSION}"\` at the top of the file`;
|
|
133
|
-
}
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
function readDeclaredVersion(data) {
|
|
137
|
-
if (data && typeof data === 'object' && 'version' in data) {
|
|
138
|
-
const v = data.version;
|
|
139
|
-
if (typeof v === 'string')
|
|
140
|
-
return v;
|
|
141
|
-
}
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
function unsupportedVersionResult(loaded, declared) {
|
|
145
|
-
const supported = SUPPORTED_POLICY_SCHEMA_VERSIONS.map((v) => `"${v}"`).join(' / ');
|
|
146
|
-
const isLegacy = declared === '0.1';
|
|
147
|
-
const hint = isLegacy
|
|
148
|
-
? `v0.1 policy support was removed in v3.0. Run \`switchbot policy migrate\` with CLI ≤2.15 first, then upgrade.`
|
|
149
|
-
: `supported versions: ${supported}. upgrade the CLI or downgrade the file.`;
|
|
150
|
-
return {
|
|
151
|
-
policyPath: loaded.path,
|
|
152
|
-
schemaVersion: CURRENT_POLICY_SCHEMA_VERSION,
|
|
153
|
-
valid: false,
|
|
154
|
-
errors: [
|
|
155
|
-
{
|
|
156
|
-
path: '/version',
|
|
157
|
-
line: 1,
|
|
158
|
-
col: 1,
|
|
159
|
-
keyword: 'unsupported-version',
|
|
160
|
-
message: `policy schema version "${declared}" is not supported by this CLI`,
|
|
161
|
-
hint,
|
|
162
|
-
schemaPath: '#/properties/version',
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Walk `automation.rules[].then[]` and flag any command string whose verb
|
|
169
|
-
* appears in DESTRUCTIVE_COMMANDS. Uses the YAML doc (not the data tree) to
|
|
170
|
-
* get accurate line/col on the offending node.
|
|
171
|
-
*
|
|
172
|
-
* This is deliberately a post-ajv pass rather than a schema rule because
|
|
173
|
-
* JSON Schema cannot parse a command string and compare the verb slot to a
|
|
174
|
-
* blocklist. Keeping it in JS also lets `src/rules/destructive.ts` be the
|
|
175
|
-
* single source of truth shared with the runtime executor.
|
|
176
|
-
*/
|
|
177
|
-
function collectDestructiveRuleErrors(loaded) {
|
|
178
|
-
const data = loaded.data;
|
|
179
|
-
const rules = data?.automation?.rules;
|
|
180
|
-
if (!Array.isArray(rules))
|
|
181
|
-
return [];
|
|
182
|
-
const out = [];
|
|
183
|
-
for (let ri = 0; ri < rules.length; ri++) {
|
|
184
|
-
const rule = rules[ri];
|
|
185
|
-
const actions = Array.isArray(rule?.then) ? rule.then : [];
|
|
186
|
-
for (let ai = 0; ai < actions.length; ai++) {
|
|
187
|
-
const cmd = actions[ai]?.command;
|
|
188
|
-
if (typeof cmd !== 'string')
|
|
189
|
-
continue;
|
|
190
|
-
const verb = destructiveVerbOf(cmd);
|
|
191
|
-
if (!verb)
|
|
192
|
-
continue;
|
|
193
|
-
const instancePath = `/automation/rules/${ri}/then/${ai}/command`;
|
|
194
|
-
const segments = instancePath.slice(1).split('/');
|
|
195
|
-
const node = getNodeAt(loaded.doc, segments);
|
|
196
|
-
const range = node?.range;
|
|
197
|
-
let line;
|
|
198
|
-
let col;
|
|
199
|
-
if (range) {
|
|
200
|
-
const pos = loaded.lineCounter.linePos(range[0]);
|
|
201
|
-
line = pos.line;
|
|
202
|
-
col = pos.col;
|
|
203
|
-
}
|
|
204
|
-
const ruleName = typeof rule?.name === 'string' ? rule.name : `#${ri}`;
|
|
205
|
-
out.push({
|
|
206
|
-
path: instancePath,
|
|
207
|
-
line,
|
|
208
|
-
col,
|
|
209
|
-
keyword: 'rule-destructive-action',
|
|
210
|
-
message: `rule "${ruleName}" action #${ai} uses destructive command "${verb}"`,
|
|
211
|
-
hint: `destructive verbs (${DESTRUCTIVE_COMMANDS.join(', ')}) cannot be pre-approved in automation rules; run them via the interactive CLI so the confirmation gate fires`,
|
|
212
|
-
schemaPath: '#/properties/automation/properties/rules/items/properties/then/items/properties/command',
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
export function validateLoadedPolicy(loaded) {
|
|
219
|
-
const declared = readDeclaredVersion(loaded.data);
|
|
220
|
-
if (declared !== undefined && !isSupportedPolicySchemaVersion(declared)) {
|
|
221
|
-
return unsupportedVersionResult(loaded, declared);
|
|
222
|
-
}
|
|
223
|
-
const version = isSupportedPolicySchemaVersion(declared)
|
|
224
|
-
? declared
|
|
225
|
-
: CURRENT_POLICY_SCHEMA_VERSION;
|
|
226
|
-
const { validate } = getValidator(version);
|
|
227
|
-
const ok = validate(loaded.data);
|
|
228
|
-
const errors = [];
|
|
229
|
-
if (!ok && validate.errors) {
|
|
230
|
-
for (const err of validate.errors) {
|
|
231
|
-
const { line, col } = locateError(loaded.doc, loaded.lineCounter, err);
|
|
232
|
-
errors.push({
|
|
233
|
-
path: err.instancePath || '',
|
|
234
|
-
line,
|
|
235
|
-
col,
|
|
236
|
-
keyword: err.keyword,
|
|
237
|
-
message: humanMessage(err),
|
|
238
|
-
hint: hintFor(err),
|
|
239
|
-
schemaPath: err.schemaPath,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// v0.2-only post-hook: destructive verbs like `unlock` / `factoryReset`
|
|
244
|
-
// cannot be pre-approved via rules, even if ajv considers the command
|
|
245
|
-
// string well-formed. Schema can't express this because `command` is a
|
|
246
|
-
// free-form string; we parse the verb in JS and append errors.
|
|
247
|
-
if (version === '0.2') {
|
|
248
|
-
const ruleErrors = collectDestructiveRuleErrors(loaded);
|
|
249
|
-
errors.push(...ruleErrors);
|
|
250
|
-
}
|
|
251
|
-
const valid = ok === true && errors.length === 0;
|
|
252
|
-
return {
|
|
253
|
-
policyPath: loaded.path,
|
|
254
|
-
schemaVersion: version,
|
|
255
|
-
valid,
|
|
256
|
-
errors,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
export function validatePolicyFile(policyPath) {
|
|
260
|
-
const loaded = loadPolicyFile(policyPath);
|
|
261
|
-
return validateLoadedPolicy(loaded);
|
|
262
|
-
}
|