@switchbot/openapi-cli 3.2.1 → 3.2.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 +3 -1
- package/dist/index.js +57349 -170
- package/package.json +9 -5
- package/dist/api/client.d.ts +0 -31
- package/dist/api/client.js +0 -236
- package/dist/api/client.js.map +0 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js +0 -21
- package/dist/auth.js.map +0 -1
- package/dist/commands/agent-bootstrap.d.ts +0 -10
- package/dist/commands/agent-bootstrap.js +0 -200
- package/dist/commands/agent-bootstrap.js.map +0 -1
- package/dist/commands/auth.d.ts +0 -18
- package/dist/commands/auth.js +0 -355
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -2
- package/dist/commands/batch.js +0 -414
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/cache.d.ts +0 -2
- package/dist/commands/cache.js +0 -127
- package/dist/commands/cache.js.map +0 -1
- package/dist/commands/capabilities.d.ts +0 -31
- package/dist/commands/capabilities.js +0 -383
- package/dist/commands/capabilities.js.map +0 -1
- package/dist/commands/catalog.d.ts +0 -2
- package/dist/commands/catalog.js +0 -360
- package/dist/commands/catalog.js.map +0 -1
- package/dist/commands/completion.d.ts +0 -2
- package/dist/commands/completion.js +0 -386
- package/dist/commands/completion.js.map +0 -1
- package/dist/commands/config.d.ts +0 -21
- package/dist/commands/config.js +0 -377
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/daemon.d.ts +0 -2
- package/dist/commands/daemon.js +0 -411
- package/dist/commands/daemon.js.map +0 -1
- package/dist/commands/device-meta.d.ts +0 -2
- package/dist/commands/device-meta.js +0 -160
- package/dist/commands/device-meta.js.map +0 -1
- package/dist/commands/devices.d.ts +0 -2
- package/dist/commands/devices.js +0 -949
- package/dist/commands/devices.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -3
- package/dist/commands/doctor.js +0 -1016
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/events.d.ts +0 -31
- package/dist/commands/events.js +0 -564
- package/dist/commands/events.js.map +0 -1
- package/dist/commands/expand.d.ts +0 -2
- package/dist/commands/expand.js +0 -131
- package/dist/commands/expand.js.map +0 -1
- package/dist/commands/explain.d.ts +0 -2
- package/dist/commands/explain.js +0 -140
- package/dist/commands/explain.js.map +0 -1
- package/dist/commands/health.d.ts +0 -8
- package/dist/commands/health.js +0 -114
- package/dist/commands/health.js.map +0 -1
- package/dist/commands/history.d.ts +0 -2
- package/dist/commands/history.js +0 -321
- package/dist/commands/history.js.map +0 -1
- package/dist/commands/identity.d.ts +0 -45
- package/dist/commands/identity.js +0 -60
- package/dist/commands/identity.js.map +0 -1
- package/dist/commands/install.d.ts +0 -20
- package/dist/commands/install.js +0 -247
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/mcp.d.ts +0 -14
- package/dist/commands/mcp.js +0 -2018
- package/dist/commands/mcp.js.map +0 -1
- package/dist/commands/plan.d.ts +0 -51
- package/dist/commands/plan.js +0 -654
- package/dist/commands/plan.js.map +0 -1
- package/dist/commands/policy.d.ts +0 -24
- package/dist/commands/policy.js +0 -587
- package/dist/commands/policy.js.map +0 -1
- package/dist/commands/quota.d.ts +0 -2
- package/dist/commands/quota.js +0 -79
- package/dist/commands/quota.js.map +0 -1
- package/dist/commands/rules.d.ts +0 -2
- package/dist/commands/rules.js +0 -876
- package/dist/commands/rules.js.map +0 -1
- package/dist/commands/scenes.d.ts +0 -2
- package/dist/commands/scenes.js +0 -265
- package/dist/commands/scenes.js.map +0 -1
- package/dist/commands/schema.d.ts +0 -2
- package/dist/commands/schema.js +0 -185
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/status-sync.d.ts +0 -2
- package/dist/commands/status-sync.js +0 -132
- package/dist/commands/status-sync.js.map +0 -1
- package/dist/commands/uninstall.d.ts +0 -20
- package/dist/commands/uninstall.js +0 -238
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/upgrade-check.d.ts +0 -2
- package/dist/commands/upgrade-check.js +0 -107
- package/dist/commands/upgrade-check.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -2
- package/dist/commands/watch.js +0 -195
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/webhook.d.ts +0 -2
- package/dist/commands/webhook.js +0 -183
- package/dist/commands/webhook.js.map +0 -1
- package/dist/config.d.ts +0 -57
- package/dist/config.js +0 -259
- package/dist/config.js.map +0 -1
- package/dist/credentials/backends/file.d.ts +0 -18
- package/dist/credentials/backends/file.js +0 -102
- package/dist/credentials/backends/file.js.map +0 -1
- package/dist/credentials/backends/linux.d.ts +0 -16
- package/dist/credentials/backends/linux.js +0 -130
- package/dist/credentials/backends/linux.js.map +0 -1
- package/dist/credentials/backends/macos.d.ts +0 -18
- package/dist/credentials/backends/macos.js +0 -130
- package/dist/credentials/backends/macos.js.map +0 -1
- package/dist/credentials/backends/windows.d.ts +0 -23
- package/dist/credentials/backends/windows.js +0 -216
- package/dist/credentials/backends/windows.js.map +0 -1
- package/dist/credentials/keychain.d.ts +0 -83
- package/dist/credentials/keychain.js +0 -89
- package/dist/credentials/keychain.js.map +0 -1
- package/dist/credentials/prime.d.ts +0 -32
- package/dist/credentials/prime.js +0 -53
- package/dist/credentials/prime.js.map +0 -1
- package/dist/devices/cache.d.ts +0 -79
- package/dist/devices/cache.js +0 -294
- package/dist/devices/cache.js.map +0 -1
- package/dist/devices/catalog.d.ts +0 -138
- package/dist/devices/catalog.js +0 -768
- package/dist/devices/catalog.js.map +0 -1
- package/dist/devices/device-meta.d.ts +0 -15
- package/dist/devices/device-meta.js +0 -57
- package/dist/devices/device-meta.js.map +0 -1
- package/dist/devices/history-agg.d.ts +0 -37
- package/dist/devices/history-agg.js +0 -139
- package/dist/devices/history-agg.js.map +0 -1
- package/dist/devices/history-query.d.ts +0 -45
- package/dist/devices/history-query.js +0 -182
- package/dist/devices/history-query.js.map +0 -1
- package/dist/devices/param-validator.d.ts +0 -40
- package/dist/devices/param-validator.js +0 -434
- package/dist/devices/param-validator.js.map +0 -1
- package/dist/devices/resources.d.ts +0 -74
- package/dist/devices/resources.js +0 -271
- package/dist/devices/resources.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js.map +0 -1
- package/dist/install/default-steps.d.ts +0 -66
- package/dist/install/default-steps.js +0 -258
- package/dist/install/default-steps.js.map +0 -1
- package/dist/install/preflight.d.ts +0 -60
- package/dist/install/preflight.js +0 -213
- package/dist/install/preflight.js.map +0 -1
- package/dist/install/steps.d.ts +0 -61
- package/dist/install/steps.js +0 -68
- package/dist/install/steps.js.map +0 -1
- package/dist/lib/command-keywords.d.ts +0 -5
- package/dist/lib/command-keywords.js +0 -18
- package/dist/lib/command-keywords.js.map +0 -1
- package/dist/lib/daemon-state.d.ts +0 -24
- package/dist/lib/daemon-state.js +0 -47
- package/dist/lib/daemon-state.js.map +0 -1
- package/dist/lib/destructive-mode.d.ts +0 -2
- package/dist/lib/destructive-mode.js +0 -13
- package/dist/lib/destructive-mode.js.map +0 -1
- package/dist/lib/devices.d.ts +0 -151
- package/dist/lib/devices.js +0 -383
- package/dist/lib/devices.js.map +0 -1
- package/dist/lib/idempotency.d.ts +0 -46
- package/dist/lib/idempotency.js +0 -107
- package/dist/lib/idempotency.js.map +0 -1
- package/dist/lib/plan-store.d.ts +0 -19
- package/dist/lib/plan-store.js +0 -69
- package/dist/lib/plan-store.js.map +0 -1
- package/dist/lib/request-context.d.ts +0 -7
- package/dist/lib/request-context.js +0 -13
- package/dist/lib/request-context.js.map +0 -1
- package/dist/lib/scenes.d.ts +0 -7
- package/dist/lib/scenes.js +0 -11
- package/dist/lib/scenes.js.map +0 -1
- package/dist/logger.d.ts +0 -4
- package/dist/logger.js +0 -17
- package/dist/logger.js.map +0 -1
- package/dist/mcp/device-history.d.ts +0 -36
- package/dist/mcp/device-history.js +0 -146
- package/dist/mcp/device-history.js.map +0 -1
- package/dist/mcp/events-subscription.d.ts +0 -45
- package/dist/mcp/events-subscription.js +0 -214
- package/dist/mcp/events-subscription.js.map +0 -1
- package/dist/mqtt/client.d.ts +0 -25
- package/dist/mqtt/client.js +0 -181
- package/dist/mqtt/client.js.map +0 -1
- package/dist/mqtt/credential.d.ts +0 -16
- package/dist/mqtt/credential.js +0 -31
- package/dist/mqtt/credential.js.map +0 -1
- package/dist/policy/add-rule.d.ts +0 -21
- package/dist/policy/add-rule.js +0 -125
- package/dist/policy/add-rule.js.map +0 -1
- package/dist/policy/diff.d.ts +0 -21
- package/dist/policy/diff.js +0 -92
- package/dist/policy/diff.js.map +0 -1
- package/dist/policy/format.d.ts +0 -6
- package/dist/policy/format.js +0 -58
- package/dist/policy/format.js.map +0 -1
- package/dist/policy/load.d.ts +0 -32
- package/dist/policy/load.js +0 -62
- package/dist/policy/load.js.map +0 -1
- package/dist/policy/migrate.d.ts +0 -21
- package/dist/policy/migrate.js +0 -68
- package/dist/policy/migrate.js.map +0 -1
- package/dist/policy/schema.d.ts +0 -5
- package/dist/policy/schema.js +0 -19
- package/dist/policy/schema.js.map +0 -1
- package/dist/policy/validate.d.ts +0 -19
- package/dist/policy/validate.js +0 -263
- package/dist/policy/validate.js.map +0 -1
- package/dist/rules/action.d.ts +0 -65
- package/dist/rules/action.js +0 -217
- package/dist/rules/action.js.map +0 -1
- package/dist/rules/audit-query.d.ts +0 -51
- package/dist/rules/audit-query.js +0 -90
- package/dist/rules/audit-query.js.map +0 -1
- package/dist/rules/conflict-analyzer.d.ts +0 -57
- package/dist/rules/conflict-analyzer.js +0 -215
- package/dist/rules/conflict-analyzer.js.map +0 -1
- package/dist/rules/cron-scheduler.d.ts +0 -62
- package/dist/rules/cron-scheduler.js +0 -187
- package/dist/rules/cron-scheduler.js.map +0 -1
- package/dist/rules/destructive.d.ts +0 -20
- package/dist/rules/destructive.js +0 -53
- package/dist/rules/destructive.js.map +0 -1
- package/dist/rules/engine.d.ts +0 -193
- package/dist/rules/engine.js +0 -758
- package/dist/rules/engine.js.map +0 -1
- package/dist/rules/matcher.d.ts +0 -56
- package/dist/rules/matcher.js +0 -231
- package/dist/rules/matcher.js.map +0 -1
- package/dist/rules/pid-file.d.ts +0 -43
- package/dist/rules/pid-file.js +0 -96
- package/dist/rules/pid-file.js.map +0 -1
- package/dist/rules/quiet-hours.d.ts +0 -26
- package/dist/rules/quiet-hours.js +0 -46
- package/dist/rules/quiet-hours.js.map +0 -1
- package/dist/rules/suggest.d.ts +0 -20
- package/dist/rules/suggest.js +0 -96
- package/dist/rules/suggest.js.map +0 -1
- package/dist/rules/throttle.d.ts +0 -61
- package/dist/rules/throttle.js +0 -117
- package/dist/rules/throttle.js.map +0 -1
- package/dist/rules/types.d.ts +0 -117
- package/dist/rules/types.js +0 -35
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/webhook-listener.d.ts +0 -63
- package/dist/rules/webhook-listener.js +0 -224
- package/dist/rules/webhook-listener.js.map +0 -1
- package/dist/rules/webhook-token.d.ts +0 -50
- package/dist/rules/webhook-token.js +0 -91
- package/dist/rules/webhook-token.js.map +0 -1
- package/dist/schema/field-aliases.d.ts +0 -34
- package/dist/schema/field-aliases.js +0 -132
- package/dist/schema/field-aliases.js.map +0 -1
- package/dist/sinks/dispatcher.d.ts +0 -7
- package/dist/sinks/dispatcher.js +0 -13
- package/dist/sinks/dispatcher.js.map +0 -1
- package/dist/sinks/file.d.ts +0 -6
- package/dist/sinks/file.js +0 -20
- package/dist/sinks/file.js.map +0 -1
- package/dist/sinks/format.d.ts +0 -20
- package/dist/sinks/format.js +0 -57
- package/dist/sinks/format.js.map +0 -1
- package/dist/sinks/homeassistant.d.ts +0 -18
- package/dist/sinks/homeassistant.js +0 -45
- package/dist/sinks/homeassistant.js.map +0 -1
- package/dist/sinks/openclaw.d.ts +0 -13
- package/dist/sinks/openclaw.js +0 -34
- package/dist/sinks/openclaw.js.map +0 -1
- package/dist/sinks/stdout.d.ts +0 -4
- package/dist/sinks/stdout.js +0 -6
- package/dist/sinks/stdout.js.map +0 -1
- package/dist/sinks/telegram.d.ts +0 -11
- package/dist/sinks/telegram.js +0 -29
- package/dist/sinks/telegram.js.map +0 -1
- package/dist/sinks/types.d.ts +0 -13
- package/dist/sinks/types.js +0 -2
- package/dist/sinks/types.js.map +0 -1
- package/dist/sinks/webhook.d.ts +0 -6
- package/dist/sinks/webhook.js +0 -23
- package/dist/sinks/webhook.js.map +0 -1
- package/dist/status-sync/manager.d.ts +0 -48
- package/dist/status-sync/manager.js +0 -269
- package/dist/status-sync/manager.js.map +0 -1
- package/dist/utils/arg-parsers.d.ts +0 -16
- package/dist/utils/arg-parsers.js +0 -67
- package/dist/utils/arg-parsers.js.map +0 -1
- package/dist/utils/audit.d.ts +0 -69
- package/dist/utils/audit.js +0 -122
- package/dist/utils/audit.js.map +0 -1
- package/dist/utils/filter.d.ts +0 -81
- package/dist/utils/filter.js +0 -190
- package/dist/utils/filter.js.map +0 -1
- package/dist/utils/flags.d.ts +0 -72
- package/dist/utils/flags.js +0 -187
- package/dist/utils/flags.js.map +0 -1
- package/dist/utils/format.d.ts +0 -9
- package/dist/utils/format.js +0 -118
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/health.d.ts +0 -48
- package/dist/utils/health.js +0 -102
- package/dist/utils/health.js.map +0 -1
- package/dist/utils/help-json.d.ts +0 -39
- package/dist/utils/help-json.js +0 -55
- package/dist/utils/help-json.js.map +0 -1
- package/dist/utils/name-resolver.d.ts +0 -26
- package/dist/utils/name-resolver.js +0 -138
- package/dist/utils/name-resolver.js.map +0 -1
- package/dist/utils/output.d.ts +0 -73
- package/dist/utils/output.js +0 -405
- package/dist/utils/output.js.map +0 -1
- package/dist/utils/quota.d.ts +0 -61
- package/dist/utils/quota.js +0 -228
- package/dist/utils/quota.js.map +0 -1
- package/dist/utils/redact.d.ts +0 -23
- package/dist/utils/redact.js +0 -69
- package/dist/utils/redact.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -72
- package/dist/utils/retry.js +0 -141
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/string.d.ts +0 -2
- package/dist/utils/string.js +0 -23
- package/dist/utils/string.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -5
- package/dist/version.js.map +0 -1
package/dist/commands/plan.js
DELETED
|
@@ -1,654 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import readline from 'node:readline';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { printJson, isJsonMode, handleError, exitWithError } from '../utils/output.js';
|
|
5
|
-
import { executeCommand, isDestructiveCommand } from '../lib/devices.js';
|
|
6
|
-
import { executeScene } from '../lib/scenes.js';
|
|
7
|
-
import { getCachedDevice } from '../devices/cache.js';
|
|
8
|
-
import { resolveDeviceId } from '../utils/name-resolver.js';
|
|
9
|
-
import { COMMAND_KEYWORDS } from '../lib/command-keywords.js';
|
|
10
|
-
import { savePlanRecord, loadPlanRecord, updatePlanRecord, listPlanRecords, PLANS_DIR, } from '../lib/plan-store.js';
|
|
11
|
-
import { allowsDirectDestructiveExecution, destructiveExecutionHint } from '../lib/destructive-mode.js';
|
|
12
|
-
function findDestructivePlanSteps(plan) {
|
|
13
|
-
const destructive = [];
|
|
14
|
-
for (let i = 0; i < plan.steps.length; i++) {
|
|
15
|
-
const step = plan.steps[i];
|
|
16
|
-
if (step.type !== 'command')
|
|
17
|
-
continue;
|
|
18
|
-
const resolvedDeviceId = resolveDeviceId(step.deviceId, step.deviceName);
|
|
19
|
-
const deviceType = getCachedDevice(resolvedDeviceId)?.type;
|
|
20
|
-
const commandType = step.commandType ?? 'command';
|
|
21
|
-
if (isDestructiveCommand(deviceType, step.command, commandType)) {
|
|
22
|
-
destructive.push({ index: i + 1, deviceId: resolvedDeviceId, command: step.command, commandType, deviceType: deviceType ?? null });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return destructive;
|
|
26
|
-
}
|
|
27
|
-
const PLAN_JSON_SCHEMA = {
|
|
28
|
-
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
29
|
-
$id: 'https://switchbot.dev/plan-1.0.json',
|
|
30
|
-
title: 'SwitchBot Plan',
|
|
31
|
-
description: 'Declarative batch of SwitchBot operations. Agent-authored; CLI validates and executes. No LLM inside the CLI — the schema is the contract.',
|
|
32
|
-
type: 'object',
|
|
33
|
-
required: ['version', 'steps'],
|
|
34
|
-
properties: {
|
|
35
|
-
version: { const: '1.0' },
|
|
36
|
-
description: { type: 'string' },
|
|
37
|
-
steps: {
|
|
38
|
-
type: 'array',
|
|
39
|
-
items: {
|
|
40
|
-
oneOf: [
|
|
41
|
-
{
|
|
42
|
-
type: 'object',
|
|
43
|
-
required: ['type', 'command'],
|
|
44
|
-
oneOf: [
|
|
45
|
-
{ required: ['deviceId'], not: { required: ['deviceName'] } },
|
|
46
|
-
{ required: ['deviceName'], not: { required: ['deviceId'] } },
|
|
47
|
-
],
|
|
48
|
-
properties: {
|
|
49
|
-
type: { const: 'command' },
|
|
50
|
-
deviceId: { type: 'string', minLength: 1 },
|
|
51
|
-
deviceName: { type: 'string', minLength: 1 },
|
|
52
|
-
command: { type: 'string', minLength: 1 },
|
|
53
|
-
parameter: {},
|
|
54
|
-
commandType: { enum: ['command', 'customize'] },
|
|
55
|
-
note: { type: 'string' },
|
|
56
|
-
},
|
|
57
|
-
additionalProperties: false,
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
type: 'object',
|
|
61
|
-
required: ['type', 'sceneId'],
|
|
62
|
-
properties: {
|
|
63
|
-
type: { const: 'scene' },
|
|
64
|
-
sceneId: { type: 'string', minLength: 1 },
|
|
65
|
-
note: { type: 'string' },
|
|
66
|
-
},
|
|
67
|
-
additionalProperties: false,
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
type: 'object',
|
|
71
|
-
required: ['type', 'ms'],
|
|
72
|
-
properties: {
|
|
73
|
-
type: { const: 'wait' },
|
|
74
|
-
ms: { type: 'integer', minimum: 0, maximum: 600000 },
|
|
75
|
-
note: { type: 'string' },
|
|
76
|
-
},
|
|
77
|
-
additionalProperties: false,
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
additionalProperties: false,
|
|
84
|
-
};
|
|
85
|
-
export function validatePlan(raw) {
|
|
86
|
-
const issues = [];
|
|
87
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
88
|
-
return { ok: false, issues: [{ path: '$', message: 'plan must be a JSON object' }] };
|
|
89
|
-
}
|
|
90
|
-
const p = raw;
|
|
91
|
-
if (p.version !== '1.0') {
|
|
92
|
-
issues.push({ path: 'version', message: 'must equal "1.0"' });
|
|
93
|
-
}
|
|
94
|
-
if (!Array.isArray(p.steps)) {
|
|
95
|
-
issues.push({ path: 'steps', message: 'must be an array' });
|
|
96
|
-
return { ok: false, issues };
|
|
97
|
-
}
|
|
98
|
-
p.steps.forEach((step, i) => {
|
|
99
|
-
const at = `steps[${i}]`;
|
|
100
|
-
if (!step || typeof step !== 'object') {
|
|
101
|
-
issues.push({ path: at, message: 'must be an object' });
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const s = step;
|
|
105
|
-
switch (s.type) {
|
|
106
|
-
case 'command':
|
|
107
|
-
if (s.deviceId !== undefined && (typeof s.deviceId !== 'string' || !s.deviceId)) {
|
|
108
|
-
issues.push({ path: `${at}.deviceId`, message: 'must be a non-empty string when provided' });
|
|
109
|
-
}
|
|
110
|
-
if (s.deviceName !== undefined && (typeof s.deviceName !== 'string' || !s.deviceName)) {
|
|
111
|
-
issues.push({ path: `${at}.deviceName`, message: 'must be a non-empty string when provided' });
|
|
112
|
-
}
|
|
113
|
-
if (!s.deviceId && !s.deviceName) {
|
|
114
|
-
issues.push({ path: `${at}`, message: 'must have either "deviceId" or "deviceName"' });
|
|
115
|
-
}
|
|
116
|
-
if (s.deviceId && s.deviceName) {
|
|
117
|
-
issues.push({ path: `${at}`, message: '"deviceId" and "deviceName" cannot both be set' });
|
|
118
|
-
}
|
|
119
|
-
if (typeof s.command !== 'string' || !s.command) {
|
|
120
|
-
issues.push({ path: `${at}.command`, message: 'must be a non-empty string' });
|
|
121
|
-
}
|
|
122
|
-
if (s.commandType !== undefined &&
|
|
123
|
-
s.commandType !== 'command' &&
|
|
124
|
-
s.commandType !== 'customize') {
|
|
125
|
-
issues.push({
|
|
126
|
-
path: `${at}.commandType`,
|
|
127
|
-
message: 'must be "command" or "customize"',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
break;
|
|
131
|
-
case 'scene':
|
|
132
|
-
if (typeof s.sceneId !== 'string' || !s.sceneId) {
|
|
133
|
-
issues.push({ path: `${at}.sceneId`, message: 'must be a non-empty string' });
|
|
134
|
-
}
|
|
135
|
-
break;
|
|
136
|
-
case 'wait':
|
|
137
|
-
if (typeof s.ms !== 'number' || !Number.isInteger(s.ms) || s.ms < 0 || s.ms > 600_000) {
|
|
138
|
-
issues.push({
|
|
139
|
-
path: `${at}.ms`,
|
|
140
|
-
message: 'must be an integer in [0, 600000]',
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
default:
|
|
145
|
-
issues.push({
|
|
146
|
-
path: `${at}.type`,
|
|
147
|
-
message: 'must be one of "command" | "scene" | "wait"',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
if (issues.length > 0)
|
|
152
|
-
return { ok: false, issues };
|
|
153
|
-
return { ok: true, plan: raw };
|
|
154
|
-
}
|
|
155
|
-
export function suggestPlan(opts) {
|
|
156
|
-
const warnings = [];
|
|
157
|
-
let command = '';
|
|
158
|
-
for (const k of COMMAND_KEYWORDS) {
|
|
159
|
-
if (k.pattern.test(opts.intent)) {
|
|
160
|
-
command = k.command;
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (!command) {
|
|
165
|
-
command = 'turnOn';
|
|
166
|
-
warnings.push(`Could not infer command from intent "${opts.intent}" — defaulted to "turnOn". Edit the generated plan to set the correct command.`);
|
|
167
|
-
}
|
|
168
|
-
const steps = opts.devices.map((d) => ({
|
|
169
|
-
type: 'command',
|
|
170
|
-
deviceId: d.id,
|
|
171
|
-
command,
|
|
172
|
-
}));
|
|
173
|
-
return { plan: { version: '1.0', description: opts.intent, steps }, warnings };
|
|
174
|
-
}
|
|
175
|
-
async function readPlanSource(file) {
|
|
176
|
-
const text = file === undefined || file === '-'
|
|
177
|
-
? await readStdin()
|
|
178
|
-
: fs.readFileSync(file, 'utf8');
|
|
179
|
-
if (!text.trim()) {
|
|
180
|
-
throw new Error(file === undefined || file === '-'
|
|
181
|
-
? 'no plan received on stdin'
|
|
182
|
-
: `plan file is empty: ${file}`);
|
|
183
|
-
}
|
|
184
|
-
try {
|
|
185
|
-
return JSON.parse(text);
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
throw new Error(`plan is not valid JSON: ${err.message}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function readStdin() {
|
|
192
|
-
return new Promise((resolve, reject) => {
|
|
193
|
-
let buf = '';
|
|
194
|
-
process.stdin.setEncoding('utf8');
|
|
195
|
-
process.stdin.on('data', (chunk) => (buf += chunk));
|
|
196
|
-
process.stdin.on('end', () => resolve(buf));
|
|
197
|
-
process.stdin.on('error', reject);
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
async function promptApproval(stepIdx, command, deviceId) {
|
|
201
|
-
if (!process.stdin.isTTY)
|
|
202
|
-
return false;
|
|
203
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
204
|
-
return new Promise((resolve) => {
|
|
205
|
-
rl.question(` Approve step ${stepIdx} — ${command} on ${deviceId}? [y/N] `, (answer) => {
|
|
206
|
-
rl.close();
|
|
207
|
-
resolve(answer.trim().toLowerCase() === 'y');
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
/** Shared plan-execution core used by both `plan run` and `plan execute`. */
|
|
212
|
-
async function executePlanSteps(plan, planId, options) {
|
|
213
|
-
const out = {
|
|
214
|
-
plan,
|
|
215
|
-
results: [],
|
|
216
|
-
summary: { total: plan.steps.length, ok: 0, error: 0, skipped: 0 },
|
|
217
|
-
};
|
|
218
|
-
for (let i = 0; i < plan.steps.length; i++) {
|
|
219
|
-
const step = plan.steps[i];
|
|
220
|
-
const idx = i + 1;
|
|
221
|
-
if (step.type === 'wait') {
|
|
222
|
-
await new Promise((r) => setTimeout(r, step.ms));
|
|
223
|
-
out.results.push({ step: idx, type: 'wait', ms: step.ms, status: 'ok' });
|
|
224
|
-
out.summary.ok++;
|
|
225
|
-
if (!isJsonMode())
|
|
226
|
-
console.log(` ${idx}. wait ${step.ms}ms`);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (step.type === 'scene') {
|
|
230
|
-
try {
|
|
231
|
-
await executeScene(step.sceneId);
|
|
232
|
-
out.results.push({ step: idx, type: 'scene', sceneId: step.sceneId, status: 'ok' });
|
|
233
|
-
out.summary.ok++;
|
|
234
|
-
if (!isJsonMode())
|
|
235
|
-
console.log(` ${idx}. ✓ scene ${step.sceneId}`);
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
239
|
-
out.results.push({ step: idx, type: 'scene', sceneId: step.sceneId, status: 'error', error: msg });
|
|
240
|
-
out.summary.error++;
|
|
241
|
-
if (!isJsonMode())
|
|
242
|
-
console.log(` ${idx}. ✗ scene ${step.sceneId}: ${msg}`);
|
|
243
|
-
if (!options.continueOnError)
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const resolvedDeviceId = resolveDeviceId(step.deviceId, step.deviceName);
|
|
249
|
-
const deviceType = getCachedDevice(resolvedDeviceId)?.type;
|
|
250
|
-
const commandType = step.commandType ?? 'command';
|
|
251
|
-
const destructive = isDestructiveCommand(deviceType, step.command, commandType);
|
|
252
|
-
let approvalDecision;
|
|
253
|
-
if (destructive && !options.yes) {
|
|
254
|
-
if (options.requireApproval) {
|
|
255
|
-
const approved = await promptApproval(idx, step.command, resolvedDeviceId);
|
|
256
|
-
if (approved) {
|
|
257
|
-
approvalDecision = 'approved';
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'skipped', error: 'destructive — rejected at prompt', decision: 'rejected' });
|
|
261
|
-
out.summary.skipped++;
|
|
262
|
-
if (!isJsonMode())
|
|
263
|
-
console.log(` ${idx}. ✗ skipped ${step.command} on ${resolvedDeviceId} (rejected)`);
|
|
264
|
-
if (!options.continueOnError)
|
|
265
|
-
break;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'skipped', error: 'destructive — rerun with --yes' });
|
|
271
|
-
out.summary.skipped++;
|
|
272
|
-
if (!isJsonMode())
|
|
273
|
-
console.log(` ${idx}. ⚠ skipped ${step.command} on ${resolvedDeviceId} (destructive — pass --yes)`);
|
|
274
|
-
if (!options.continueOnError)
|
|
275
|
-
break;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
await executeCommand(resolvedDeviceId, step.command, step.parameter, commandType, undefined, { planId });
|
|
281
|
-
out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'ok', ...(approvalDecision ? { decision: approvalDecision } : {}) });
|
|
282
|
-
out.summary.ok++;
|
|
283
|
-
if (!isJsonMode())
|
|
284
|
-
console.log(` ${idx}. ✓ ${step.command} on ${resolvedDeviceId}`);
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
if (err instanceof Error && err.name === 'DryRunSignal') {
|
|
288
|
-
out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'ok' });
|
|
289
|
-
out.summary.ok++;
|
|
290
|
-
if (!isJsonMode())
|
|
291
|
-
console.log(` ${idx}. ◦ dry-run ${step.command} on ${resolvedDeviceId}`);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
295
|
-
out.results.push({ step: idx, type: 'command', deviceId: resolvedDeviceId, command: step.command, status: 'error', error: msg });
|
|
296
|
-
out.summary.error++;
|
|
297
|
-
if (!isJsonMode())
|
|
298
|
-
console.log(` ${idx}. ✗ ${step.command} on ${resolvedDeviceId}: ${msg}`);
|
|
299
|
-
if (!options.continueOnError)
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
export function registerPlanCommand(program) {
|
|
306
|
-
const plan = program
|
|
307
|
-
.command('plan')
|
|
308
|
-
.description('Author, validate, and run SwitchBot batch plans (JSON schema for AI agents)')
|
|
309
|
-
.addHelpText('after', `
|
|
310
|
-
A "plan" is a JSON document describing a sequence of commands/scenes/waits.
|
|
311
|
-
The schema is fixed — agents emit plans, the CLI executes them. No LLM inside.
|
|
312
|
-
|
|
313
|
-
{ "version": "1.0", "description": "...", "steps": [
|
|
314
|
-
{ "type": "command", "deviceId": "...", "command": "turnOff" },
|
|
315
|
-
{ "type": "wait", "ms": 500 },
|
|
316
|
-
{ "type": "scene", "sceneId": "..." }
|
|
317
|
-
]}
|
|
318
|
-
|
|
319
|
-
Workflow:
|
|
320
|
-
$ switchbot plan schema > plan.schema.json # export the contract
|
|
321
|
-
$ switchbot plan validate my-plan.json # check shape without running
|
|
322
|
-
$ switchbot --dry-run plan run my-plan.json # preview (mutations skipped)
|
|
323
|
-
$ switchbot plan save my-plan.json # store a reviewed plan
|
|
324
|
-
$ switchbot plan review <planId>
|
|
325
|
-
$ switchbot plan approve <planId>
|
|
326
|
-
$ switchbot plan execute <planId>
|
|
327
|
-
$ cat plan.json | switchbot plan run - # or stream via stdin
|
|
328
|
-
`);
|
|
329
|
-
plan
|
|
330
|
-
.command('schema')
|
|
331
|
-
.description('Print the JSON Schema for the plan format')
|
|
332
|
-
.action(() => {
|
|
333
|
-
printJson({
|
|
334
|
-
...PLAN_JSON_SCHEMA,
|
|
335
|
-
agentNotes: {
|
|
336
|
-
deviceNameStrategy: "Plan step `deviceName` fields are resolved with the `require-unique` strategy (same default as `devices command`). Plans that expect a specific device should pin `deviceId` instead.",
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
plan
|
|
341
|
-
.command('validate')
|
|
342
|
-
.description('Validate a plan file (or stdin) against the schema (structural only; does not verify device or scene existence)')
|
|
343
|
-
.argument('[file]', 'Path to plan.json, or "-" / omit to read stdin')
|
|
344
|
-
.addHelpText('after', `
|
|
345
|
-
To check semantic validity (e.g., that deviceIds and sceneIds actually exist),
|
|
346
|
-
use 'plan run --dry-run' which exercises name resolution and device lookup
|
|
347
|
-
against the live API without executing any mutations.
|
|
348
|
-
`)
|
|
349
|
-
.action(async (file) => {
|
|
350
|
-
let raw;
|
|
351
|
-
try {
|
|
352
|
-
raw = await readPlanSource(file);
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
handleError(err);
|
|
356
|
-
}
|
|
357
|
-
const result = validatePlan(raw);
|
|
358
|
-
if (!result.ok) {
|
|
359
|
-
if (isJsonMode()) {
|
|
360
|
-
printJson({ valid: false, issues: result.issues });
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
console.error('✗ plan invalid:');
|
|
364
|
-
for (const i of result.issues) {
|
|
365
|
-
console.error(` ${i.path}: ${i.message}`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
process.exit(2);
|
|
369
|
-
}
|
|
370
|
-
if (isJsonMode()) {
|
|
371
|
-
const out = { valid: true, steps: result.plan.steps.length };
|
|
372
|
-
if (result.plan.steps.length === 0)
|
|
373
|
-
out.warning = 'plan has no steps — nothing will execute';
|
|
374
|
-
printJson(out);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
if (result.plan.steps.length === 0) {
|
|
378
|
-
console.log('✓ plan valid — but 0 steps: nothing will execute');
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
console.log(`✓ plan valid (${result.plan.steps.length} step${result.plan.steps.length === 1 ? '' : 's'})`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
plan
|
|
386
|
-
.command('suggest')
|
|
387
|
-
.description('Generate a candidate Plan JSON from intent + devices (heuristic, no LLM)')
|
|
388
|
-
.requiredOption('--intent <text>', 'Natural language description (e.g. "turn off all lights")')
|
|
389
|
-
.option('--device <id>', 'Device ID to include (repeatable)', (v, prev) => [...prev, v], [])
|
|
390
|
-
.option('--out <file>', 'Write plan JSON to file instead of stdout')
|
|
391
|
-
.action((opts) => {
|
|
392
|
-
if (opts.device.length === 0) {
|
|
393
|
-
console.error('error: at least one --device is required');
|
|
394
|
-
process.exit(1);
|
|
395
|
-
}
|
|
396
|
-
const devices = opts.device.map((ref) => {
|
|
397
|
-
const cached = getCachedDevice(ref);
|
|
398
|
-
return { id: ref, name: cached?.name, type: cached?.type };
|
|
399
|
-
});
|
|
400
|
-
const { plan: suggested, warnings } = suggestPlan({ intent: opts.intent, devices });
|
|
401
|
-
for (const w of warnings)
|
|
402
|
-
process.stderr.write(`warning: ${w}\n`);
|
|
403
|
-
const json = JSON.stringify(suggested, null, 2);
|
|
404
|
-
if (opts.out) {
|
|
405
|
-
fs.writeFileSync(opts.out, json + '\n', 'utf8');
|
|
406
|
-
if (!isJsonMode())
|
|
407
|
-
console.log(`✓ plan written to ${opts.out}`);
|
|
408
|
-
}
|
|
409
|
-
else if (isJsonMode()) {
|
|
410
|
-
printJson({ plan: suggested, warnings });
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
console.log(json);
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
plan
|
|
417
|
-
.command('run')
|
|
418
|
-
.description('Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default')
|
|
419
|
-
.argument('[file]', 'Path to plan.json, or "-" / omit to read stdin')
|
|
420
|
-
.option('--yes', 'Authorize destructive commands (e.g. Smart Lock unlock, Garage open)')
|
|
421
|
-
.option('--require-approval', 'Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)')
|
|
422
|
-
.option('--continue-on-error', 'Keep running after a failed step (default: stop at first error)')
|
|
423
|
-
.action(async (file, options) => {
|
|
424
|
-
if (options.requireApproval && isJsonMode()) {
|
|
425
|
-
console.error('error: --require-approval cannot be used with --json (no TTY available for prompts)');
|
|
426
|
-
process.exit(1);
|
|
427
|
-
}
|
|
428
|
-
let raw;
|
|
429
|
-
try {
|
|
430
|
-
raw = await readPlanSource(file);
|
|
431
|
-
}
|
|
432
|
-
catch (err) {
|
|
433
|
-
handleError(err);
|
|
434
|
-
}
|
|
435
|
-
const v = validatePlan(raw);
|
|
436
|
-
if (!v.ok) {
|
|
437
|
-
if (isJsonMode()) {
|
|
438
|
-
printJson({ ran: false, issues: v.issues });
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
console.error('✗ plan invalid, refusing to run:');
|
|
442
|
-
for (const i of v.issues)
|
|
443
|
-
console.error(` ${i.path}: ${i.message}`);
|
|
444
|
-
}
|
|
445
|
-
process.exit(2);
|
|
446
|
-
}
|
|
447
|
-
const planId = randomUUID();
|
|
448
|
-
const destructiveSteps = findDestructivePlanSteps(v.plan);
|
|
449
|
-
if (options.yes && destructiveSteps.length > 0 && !allowsDirectDestructiveExecution()) {
|
|
450
|
-
exitWithError({
|
|
451
|
-
code: 2,
|
|
452
|
-
kind: 'guard',
|
|
453
|
-
message: `Direct destructive execution is disabled for plan run (${destructiveSteps.length} destructive step${destructiveSteps.length === 1 ? '' : 's'}).`,
|
|
454
|
-
hint: destructiveExecutionHint(),
|
|
455
|
-
context: {
|
|
456
|
-
planId,
|
|
457
|
-
destructiveSteps: destructiveSteps.map((step) => ({
|
|
458
|
-
step: step.index,
|
|
459
|
-
deviceId: step.deviceId,
|
|
460
|
-
deviceType: step.deviceType,
|
|
461
|
-
command: step.command,
|
|
462
|
-
commandType: step.commandType,
|
|
463
|
-
})),
|
|
464
|
-
requiredWorkflow: 'plan-approval',
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
let out;
|
|
469
|
-
try {
|
|
470
|
-
out = await executePlanSteps(v.plan, planId, options);
|
|
471
|
-
if (isJsonMode()) {
|
|
472
|
-
printJson({ ran: true, planId, ...out });
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
const { ok, error, skipped, total } = out.summary;
|
|
476
|
-
console.log(`\nsummary: ok=${ok} error=${error} skipped=${skipped} total=${total}`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
catch (err) {
|
|
480
|
-
handleError(err);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
if (out.summary.error > 0)
|
|
484
|
-
process.exit(1);
|
|
485
|
-
});
|
|
486
|
-
// ---- Plan resource-model subcommands (P0-3) --------------------------------
|
|
487
|
-
plan
|
|
488
|
-
.command('save')
|
|
489
|
-
.description('Save a plan JSON to ~/.switchbot/plans/ with status=pending (waiting for approval).')
|
|
490
|
-
.argument('[file]', 'Path to plan.json, or "-" / omit to read stdin')
|
|
491
|
-
.action(async (file) => {
|
|
492
|
-
let raw;
|
|
493
|
-
try {
|
|
494
|
-
raw = await readPlanSource(file);
|
|
495
|
-
}
|
|
496
|
-
catch (err) {
|
|
497
|
-
handleError(err);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const v = validatePlan(raw);
|
|
501
|
-
if (!v.ok) {
|
|
502
|
-
exitWithError({
|
|
503
|
-
code: 2, kind: 'usage',
|
|
504
|
-
message: `Plan is invalid (${v.issues.length} issue${v.issues.length > 1 ? 's' : ''})`,
|
|
505
|
-
context: { issues: v.issues },
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
const record = savePlanRecord(v.plan);
|
|
509
|
-
if (isJsonMode()) {
|
|
510
|
-
printJson({ saved: true, planId: record.planId, status: record.status, createdAt: record.createdAt, plansDir: PLANS_DIR });
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
console.log(`✓ Plan saved — planId: ${record.planId}`);
|
|
514
|
-
console.log(` Status: ${record.status}`);
|
|
515
|
-
console.log(` Path: ${PLANS_DIR}/${record.planId}.json`);
|
|
516
|
-
console.log(` Next: switchbot plan review ${record.planId}`);
|
|
517
|
-
console.log(` switchbot plan approve ${record.planId}`);
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
plan
|
|
521
|
-
.command('list')
|
|
522
|
-
.description('List saved plans in ~/.switchbot/plans/ with their approval status.')
|
|
523
|
-
.action(() => {
|
|
524
|
-
const records = listPlanRecords();
|
|
525
|
-
if (isJsonMode()) {
|
|
526
|
-
printJson({ plans: records.map((r) => ({ planId: r.planId, status: r.status, createdAt: r.createdAt, approvedAt: r.approvedAt ?? null, executedAt: r.executedAt ?? null, description: r.plan.description ?? null })) });
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (records.length === 0) {
|
|
530
|
-
console.log('No saved plans. Use: switchbot plan save <file>');
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
for (const r of records) {
|
|
534
|
-
const parts = [`${r.planId.slice(0, 8)}…`, r.status, r.createdAt.slice(0, 16)];
|
|
535
|
-
if (r.plan.description)
|
|
536
|
-
parts.push(`"${r.plan.description}"`);
|
|
537
|
-
console.log(parts.join(' '));
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
plan
|
|
541
|
-
.command('review')
|
|
542
|
-
.description('Show the details of a saved plan (steps, status, approval history).')
|
|
543
|
-
.argument('<planId>', 'Plan UUID from "plan list"')
|
|
544
|
-
.action((planId) => {
|
|
545
|
-
const record = loadPlanRecord(planId);
|
|
546
|
-
if (!record) {
|
|
547
|
-
exitWithError({ code: 2, kind: 'usage', message: `Plan ${planId} not found in ${PLANS_DIR}` });
|
|
548
|
-
}
|
|
549
|
-
if (isJsonMode()) {
|
|
550
|
-
printJson(record);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
console.log(`planId: ${record.planId}`);
|
|
554
|
-
console.log(`status: ${record.status}`);
|
|
555
|
-
console.log(`createdAt: ${record.createdAt}`);
|
|
556
|
-
if (record.approvedAt)
|
|
557
|
-
console.log(`approvedAt: ${record.approvedAt}`);
|
|
558
|
-
if (record.executedAt)
|
|
559
|
-
console.log(`executedAt: ${record.executedAt}`);
|
|
560
|
-
if (record.plan.description)
|
|
561
|
-
console.log(`description: ${record.plan.description}`);
|
|
562
|
-
console.log(`steps (${record.plan.steps.length}):`);
|
|
563
|
-
for (let i = 0; i < record.plan.steps.length; i++) {
|
|
564
|
-
const step = record.plan.steps[i];
|
|
565
|
-
if (step.type === 'command') {
|
|
566
|
-
const id = step.deviceId ?? step.deviceName ?? '?';
|
|
567
|
-
console.log(` ${i + 1}. command ${step.command} on ${id}${step.note ? ` # ${step.note}` : ''}`);
|
|
568
|
-
}
|
|
569
|
-
else if (step.type === 'scene') {
|
|
570
|
-
console.log(` ${i + 1}. scene ${step.sceneId}${step.note ? ` # ${step.note}` : ''}`);
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
console.log(` ${i + 1}. wait ${step.ms}ms`);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
plan
|
|
578
|
-
.command('approve')
|
|
579
|
-
.description('Approve a saved plan, allowing `plan execute` to run it.')
|
|
580
|
-
.argument('<planId>', 'Plan UUID from "plan list"')
|
|
581
|
-
.action((planId) => {
|
|
582
|
-
const record = loadPlanRecord(planId);
|
|
583
|
-
if (!record) {
|
|
584
|
-
exitWithError({ code: 2, kind: 'usage', message: `Plan ${planId} not found in ${PLANS_DIR}` });
|
|
585
|
-
}
|
|
586
|
-
if (record.status === 'executed') {
|
|
587
|
-
exitWithError({ code: 2, kind: 'guard', message: `Plan ${planId} has already been executed.` });
|
|
588
|
-
}
|
|
589
|
-
if (record.status === 'rejected') {
|
|
590
|
-
exitWithError({ code: 2, kind: 'guard', message: `Plan ${planId} was rejected. Save a new plan to start fresh.` });
|
|
591
|
-
}
|
|
592
|
-
// 'failed' plans may be re-approved and retried — intentionally no block here.
|
|
593
|
-
const updated = updatePlanRecord(planId, { status: 'approved', approvedAt: new Date().toISOString() });
|
|
594
|
-
if (isJsonMode()) {
|
|
595
|
-
printJson({ ok: true, planId: updated.planId, status: updated.status, approvedAt: updated.approvedAt });
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
console.log(`✓ Plan ${planId.slice(0, 8)}… approved.`);
|
|
599
|
-
console.log(` Next: switchbot plan execute ${planId}`);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
plan
|
|
603
|
-
.command('execute')
|
|
604
|
-
.description('Execute a pre-approved plan. Only runs if status=approved; audit entries are tagged with planId.')
|
|
605
|
-
.argument('<planId>', 'Plan UUID from "plan list" (must be in approved status)')
|
|
606
|
-
.option('--yes', 'Deprecated no-op: approved plans already authorize destructive steps')
|
|
607
|
-
.option('--require-approval', 'Prompt for each destructive step (TTY only)')
|
|
608
|
-
.option('--continue-on-error', 'Keep running after a failed step')
|
|
609
|
-
.action(async (planId, options) => {
|
|
610
|
-
if (options.requireApproval && isJsonMode()) {
|
|
611
|
-
exitWithError({ code: 1, kind: 'usage', message: '--require-approval cannot be used with --json' });
|
|
612
|
-
}
|
|
613
|
-
const record = loadPlanRecord(planId);
|
|
614
|
-
if (!record) {
|
|
615
|
-
exitWithError({ code: 2, kind: 'usage', message: `Plan ${planId} not found in ${PLANS_DIR}` });
|
|
616
|
-
}
|
|
617
|
-
if (record.status !== 'approved') {
|
|
618
|
-
exitWithError({
|
|
619
|
-
code: 2, kind: 'guard',
|
|
620
|
-
message: `Plan ${planId.slice(0, 8)}… cannot be executed: status is "${record.status}", expected "approved".`,
|
|
621
|
-
hint: record.status === 'pending' ? `Run: switchbot plan approve ${planId}` : record.status === 'failed' ? `Re-run: switchbot plan approve ${planId}` : undefined,
|
|
622
|
-
context: { planId, status: record.status },
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
let out;
|
|
626
|
-
try {
|
|
627
|
-
out = await executePlanSteps(record.plan, planId, { ...options, yes: true });
|
|
628
|
-
}
|
|
629
|
-
catch (err) {
|
|
630
|
-
handleError(err);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const { ok, error, skipped } = out.summary;
|
|
634
|
-
const succeeded = error === 0 && skipped === 0;
|
|
635
|
-
const failureReason = succeeded ? undefined : [error > 0 ? `${error} error${error > 1 ? 's' : ''}` : null, skipped > 0 ? `${skipped} skipped` : null].filter(Boolean).join(', ');
|
|
636
|
-
if (succeeded) {
|
|
637
|
-
updatePlanRecord(planId, { status: 'executed', executedAt: new Date().toISOString() });
|
|
638
|
-
}
|
|
639
|
-
else {
|
|
640
|
-
updatePlanRecord(planId, { status: 'failed', failedAt: new Date().toISOString(), failureReason });
|
|
641
|
-
}
|
|
642
|
-
if (isJsonMode()) {
|
|
643
|
-
printJson({ ran: true, planId, succeeded, ...out });
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
console.log(`\nsummary: ok=${ok} error=${error} skipped=${skipped} total=${out.summary.total}`);
|
|
647
|
-
if (!succeeded)
|
|
648
|
-
console.error(`Plan marked as failed (${failureReason}). Re-run after fixing to retry.`);
|
|
649
|
-
}
|
|
650
|
-
if (!succeeded)
|
|
651
|
-
process.exit(1);
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
//# sourceMappingURL=plan.js.map
|