uc-config 0.1.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/docs/cli.md ADDED
@@ -0,0 +1,125 @@
1
+ # CLI reference
2
+
3
+ Run as `npm run uc -- <command>` inside this repo, or `npx uc-config <command>`
4
+ when installed as a dependency. Global option: `--workspace <dir>` (directory
5
+ holding `remote.config.ts` and `.uc/`; default: current directory).
6
+
7
+ Most commands take `--target <name>`. The default is `$UC_TARGET`; otherwise
8
+ the workspace's only target if exactly one is connected; otherwise `home`.
9
+
10
+ ## Commands
11
+
12
+ | Command | Writes to remote | Purpose |
13
+ | ---------------------------------------------- | ----------------- | ----------------------------------------------------------------------- |
14
+ | `init` | no | Scaffold a private config workspace. Never overwrites files. |
15
+ | `connect <name> --host <url>` | no | Record a target (identity, firmware). Rerun after firmware updates. |
16
+ | `auth` | API key only | Exchange the web-configurator PIN (`UC_PIN` or prompt) for an API key. |
17
+ | `doctor` | no | Verify identity and read access to every required endpoint. |
18
+ | `diagnose [--json]` | no | Orphaned entity references, disconnected integrations, suggested fixes. |
19
+ | `inventory [--out f] [--bindings f.ts]` | no | Raw (redacted) remote state; optional typed entity/command bindings. |
20
+ | `import [--out f.ts]` | no | Generate editable config from the live remote. Never overwrites. |
21
+ | `compile [--config f] [--out f]` | no | Evaluate TS config into `.uc/build.json`. Offline. |
22
+ | `plan [--out f] [--prune] [--overwrite-drift]` | no | Three-way diff: source vs last-applied vs live. |
23
+ | `apply <plan> [--adopt-only]` | yes | Execute a saved plan after re-verifying preconditions. |
24
+ | `check` | no | Re-plan and exit 2 if anything differs. |
25
+ | `resume` | maybe | Continue paused setup; reconcile an interrupted apply. |
26
+ | `setup status/respond <key>` | yes | Drive interactive integration/dock setup. |
27
+ | `pairing status/respond <remoteId>` | yes | Bluetooth pairing steps. |
28
+ | `ir learn/capture <emitterId>` | yes | Learn IR codes from a physical remote. |
29
+ | `state adopt <key> <id>` / `forget` / `move` | no | Edit local ownership bindings. |
30
+ | `rollback [--out f]` | no | Build a compensating plan from the last journal. |
31
+ | `backup --out f` | stops intgs | Native full backup. Disruptive; not for routine use. |
32
+ | `api <METHOD> <path> [--data json] [--write]` | only with --write | Raw authenticated Core API call; output redacted. |
33
+
34
+ Exit codes: `0` ok, `1` error, `2` drift/conflicts/deferred/diagnose findings,
35
+ `3` paused for a human step.
36
+
37
+ ## Authentication
38
+
39
+ `auth` prompts for the web configurator PIN. Enable the web configurator on the
40
+ remote first, and approve the key on the remote if asked. Non-interactive
41
+ options: set `UC_PIN`, or set `UC_API_KEY` to an existing key (`connect
42
+ --token-env NAME` changes the variable name). The key is stored in
43
+ `.uc/credentials.json` with mode 0600. Never commit or print it.
44
+
45
+ Configuration secrets use `secret('env:NAME')` or, on macOS,
46
+ `secret('keychain:service/account')`. An optional second argument is a
47
+ non-secret version label that triggers rotation. Raw secret values never appear
48
+ in plans, state or journals.
49
+
50
+ ## Adopting an existing remote
51
+
52
+ `import` generates config without taking ownership. The first `plan` shows
53
+ `= adopt` operations; `apply <plan> --adopt-only` records the baseline locally
54
+ and refuses any remote write. Import warnings list what can't be recovered,
55
+ such as original setup credentials, driver archives and binary assets.
56
+
57
+ ## Provisioning (new integrations, docks, remotes)
58
+
59
+ A new dependency is created before its dependents. When a plan reports
60
+ **deferred** resources, apply the current phase, then **plan again**. Newly
61
+ created activities and macros first receive their entity membership, which
62
+ exposes the command metadata needed to validate their sequences and buttons.
63
+
64
+ ```sh
65
+ npm run uc -- setup status media
66
+ npm run uc -- setup respond media --input setup-response.json # {"input_values": {...}}
67
+ npm run uc -- setup respond media --confirm
68
+ npm run uc -- resume
69
+ npm run uc -- plan
70
+ ```
71
+
72
+ Secret-bearing setup values can be written as `{"$secret":"env:DEVICE_PIN"}`.
73
+ Expired or rejected setup sessions are reported, never silently restarted.
74
+
75
+ ```sh
76
+ npm run uc -- pairing status REMOTE_ENTITY_ID
77
+ npm run uc -- pairing respond REMOTE_ENTITY_ID --input pairing-response.json # {"id":11,"passkey":{"$secret":"env:BT_PASSKEY"}}
78
+ npm run uc -- ir learn EMITTER_ID
79
+ npm run uc -- ir capture EMITTER_ID --out learned.json
80
+ ```
81
+
82
+ Installing _community_ integrations (download, version selection, updates) is
83
+ the [UC Integration Manager](https://github.com/JackJPowell/uc-intg-manager)'s
84
+ job. uc-config's `driver()` registers an already-running external driver, and
85
+ `driverArchive()` uploads a local archive.
86
+
87
+ ## Ownership, recovery and removal
88
+
89
+ - Plans compare three things: source, the last applied state and live data.
90
+ Live edits to owned fields produce conflicts. Copy the live value into source
91
+ to accept it; `plan --overwrite-drift` restores the source value instead.
92
+ - Apply verifies identity, firmware, state revision, plan integrity and live
93
+ preconditions, and re-reads before each mutation. The Core API is not
94
+ transactional, so avoid configurator edits while an apply runs.
95
+ - Per-target locks prevent two local writers. A lock left by a crashed process
96
+ is kept until someone inspects it.
97
+ - Failed or uncertain writes block the next apply until `resume` runs. A create
98
+ whose outcome is unknown is never replayed; inspect inventory and run
99
+ `state adopt KEY EXACT_ID` if it succeeded.
100
+ - `state move OLD NEW` keeps the remote ID when you rename a logical key.
101
+ `state forget KEY` gives up ownership without deleting anything.
102
+ - Removing a managed resource from source needs `plan --prune` or
103
+ `state forget`. Pruning checks inbound references and never bulk-deletes.
104
+ Integration, driver, dock and entity removal is blocked; handle it explicitly.
105
+ - `rollback` refuses when there is later drift, created or deleted resources,
106
+ fields that were previously absent, or irreversible provisioning.
107
+ Earlier journals stay in `.uc/journals/history/`.
108
+
109
+ ## Ownership rules for arrays and fields
110
+
111
+ Arrays (page items, sequences, entity membership) are owned as whole fields.
112
+ Omitting a scalar or object field gives up ownership without clearing it. To
113
+ clear a collection, declare `[]`. Page-item defaults returned by the remote are
114
+ ignored when you didn't declare them.
115
+
116
+ ## Limits
117
+
118
+ - Import is not a full disaster-recovery export. Setup secrets, driver binaries
119
+ and some assets cannot be read back.
120
+ - The API doesn't expose installed driver digests, so a driver replaced out of
121
+ band can't be fully detected.
122
+ - Immutable creation fields (e.g. IR remote codeset) need an explicit
123
+ replacement.
124
+ - Firmware installs, Wi-Fi bootstrap and external-service deployment are out of
125
+ scope.
@@ -0,0 +1,196 @@
1
+ # Authoring Remote 3 configuration
2
+
3
+ Edit `remote.config.ts`. It exports a `defineRemote({ schemaVersion: 1, resources })`
4
+ object. TypeScript constants, functions, imports, and object spreads can reuse
5
+ configuration, but the final value must be plain JSON-compatible data. Avoid
6
+ `undefined`, functions, class instances, cycles, non-finite numbers, and side effects.
7
+ Compile executes this trusted code locally; it does not install JavaScript on the remote.
8
+
9
+ ## Resource anatomy and identity
10
+
11
+ Each `resources` key is a stable local name. The resource describes native API data:
12
+
13
+ ```ts
14
+ import { defineRemote, ref } from "uc-config";
15
+
16
+ export default defineRemote({
17
+ schemaVersion: 1,
18
+ resources: {
19
+ "activity.watch_tv": {
20
+ kind: "activity",
21
+ id: "EXISTING_ACTIVITY_ID",
22
+ data: { name: { en_US: "Watch TV" } },
23
+ },
24
+ "activity.watch_tv.button.volume_up_short_press": {
25
+ kind: "activityButton",
26
+ parent: ref("activity.watch_tv"),
27
+ id: "VOLUME_UP/short_press",
28
+ data: {
29
+ short_press: {
30
+ entity_id: "EXACT_CONFIGURED_ENTITY_ID",
31
+ cmd_id: "EXACT_SUPPORTED_COMMAND_ID",
32
+ params: {},
33
+ },
34
+ },
35
+ },
36
+ },
37
+ });
38
+ ```
39
+
40
+ This is a structural example with placeholders, not a deployable setup. The command
41
+ target also needs to be in the activity's `options.entity_ids` before using the binding.
42
+
43
+ | Field | Meaning |
44
+ | ---------------------- | --------------------------------------------------------------------------------------------------------- |
45
+ | Resource map key | Local identity used by references, plans, and state; not a display name. |
46
+ | `kind` | Supported resource kind from `src/model.ts`. |
47
+ | `id` | Exact native remote ID, or kind-specific identity such as `VOLUME_UP/short_press`. Preserve existing IDs. |
48
+ | `parent` | Native parent ID or `ref("logical.key")`. |
49
+ | `data` | Desired writable fields this configuration owns. |
50
+ | `create` | Native creation/setup payload; distinct from update data. Imported resources may lack it. |
51
+ | `dependsOn` | Additional logical resource keys that must be ready first. References also establish dependencies. |
52
+ | `file`, `resourceType` | Local artifact path and asset type for the applicable helpers. Compilation records a content hash. |
53
+
54
+ Use unique keys. JavaScript object construction can overwrite duplicate keys before
55
+ validation. Renaming a key requires updating its references and running
56
+ `npm run uc -- state move OLD NEW`; changing `data.name` does not.
57
+
58
+ ## Editing the imported setup
59
+
60
+ Keep the existing native objects unless a reusable helper makes the requested change
61
+ clearer. Rewriting an imported activity with `activity()` can change its locale map
62
+ (`en_US` versus the helper's `en`), add creation inputs, or change owned fields.
63
+
64
+ - **Rename a displayed activity:** edit its existing `data.name` locale entry.
65
+ Preserve its logical key, `id`, and other locales.
66
+ - **Change a physical button:** locate its `activityButton` or `remoteButton`
67
+ resource by parent and button ID. Edit the command under `data.short_press` or
68
+ `data.long_press`; preserve the other bindings. Button IDs use uppercase names
69
+ and a press suffix, for example `HOME/long_press`.
70
+ - **Change a sequence:** edit `data.options.sequences.on` or `.off` for activities,
71
+ or `data.options.sequence` for macros. Native command steps use
72
+ `{ type: "command", command: { entity_id, cmd_id, params } }`; delays use
73
+ `{ type: "delay", delay: 500 }` (milliseconds). Preserve order and other steps.
74
+ - **Change a screen:** edit the appropriate child `activityPage` or `remotePage`.
75
+ Native items use `location: { x, y }` and optional `size: { width, height }`.
76
+ Coordinates are zero-based; items must fit the grid and not overlap.
77
+ - **Use another device:** verify its exact configured entity ID and supported
78
+ command metadata, then include it in the activity/macro's entity membership.
79
+ Display names and available integration entity IDs are not interchangeable with
80
+ configured command target IDs.
81
+
82
+ Arrays are owned as whole fields, including page items, sequences, and entity
83
+ membership. Keep unrelated entries when editing one item. Omitting a field gives
84
+ up ownership without clearing its live value; `[]` explicitly clears a collection.
85
+ Use only native-schema-supported empty values for other fields.
86
+
87
+ Do not reimport over the active source to change one setting. If live values must
88
+ be inspected, import to a new file and selectively reconcile the relevant fields.
89
+ Treat generated inventory/bindings as discovery snapshots that may need refreshing.
90
+
91
+ ## Composing new resources with helpers
92
+
93
+ The helper API produces the same resource objects. This example uses placeholders
94
+ that must be replaced with verified inventory values:
95
+
96
+ ```ts
97
+ import {
98
+ activity,
99
+ bind,
100
+ button,
101
+ command,
102
+ defineRemote,
103
+ delay,
104
+ page,
105
+ ref,
106
+ } from "uc-config";
107
+
108
+ const player = "EXACT_CONFIGURED_ENTITY_ID";
109
+ const home = command(player, "EXACT_SUPPORTED_HOME_COMMAND_ID");
110
+
111
+ export default defineRemote({
112
+ schemaVersion: 1,
113
+ resources: {
114
+ "activity.watch": activity({
115
+ name: "Watch",
116
+ entities: [player],
117
+ on: [home, delay(500)],
118
+ }),
119
+ "activity.watch.home": bind(ref("activity.watch"), "HOME", home),
120
+ "activity.watch.controls": page(ref("activity.watch"), {
121
+ name: "Controls",
122
+ grid: { width: 4, height: 6 },
123
+ items: [button({ label: "Home", at: [0, 0], command: home })],
124
+ }),
125
+ },
126
+ });
127
+ ```
128
+
129
+ Helpers `activity({ on, off })` and `macro({ steps })` wrap command steps for you;
130
+ do not pass already-wrapped native command steps to these arrays. `button()` takes
131
+ `at: [x, y]` and optional `size: [width, height]`, unlike native item objects.
132
+
133
+ `bind(parent, button, command, press, scope)` defaults to `short_press` and
134
+ `activity`. `page(parent, data, id, scope)` also defaults to activity scope.
135
+ For remote children choose `"remote"`; for commands local to that remote use
136
+ `localCommand(cmdId, params?)`, which omits `entity_id`. Verify supported commands
137
+ instead of inferring their names.
138
+
139
+ For native structures without a dedicated helper, use `resource(kind, options)`.
140
+ Exported `Schemas` exposes native request types. See [the complete template](../examples/full-setup.ts)
141
+ and [the helper implementation](../src/dsl.ts) for exact signatures.
142
+
143
+ ## Integrations and provisioning
144
+
145
+ Use the driver's advertised setup schema; there is no universal device setup payload.
146
+ `integration()` takes a driver ID/reference, name, and driver-specific `setup` data.
147
+ `entity()` selects one exact available entity using its integration parent and
148
+ `entityId`. Do not configure every discovered entity implicitly.
149
+
150
+ `driver()` registers an already-running external service; it does not deploy that
151
+ service. `driverArchive()` installs a local archive. `dock()`, `remote()`,
152
+ `pairing()`, and `irCode()` cover their respective native setup resources. See the
153
+ README and complete template before adding these resources.
154
+
155
+ Use `secret("env:VARIABLE")` or `secret("keychain:service/account")` for secret
156
+ inputs. An optional second argument is a non-secret rotation version. Never put
157
+ PINs/API keys into source or print `.uc/credentials.json`.
158
+
159
+ New resources may require multiple plan/apply phases. Integration setup and pairing
160
+ can pause for user interaction. Inspect `setup status`, provide the requested input
161
+ through `setup respond`, and use `resume` as documented in [the CLI reference](cli.md). Replan after
162
+ each completed phase; never bypass deferred dependency or command validation.
163
+
164
+ Imported configuration is not a full disaster-recovery backup: original setup
165
+ credentials, driver archives, and binary assets may be missing. Installing and
166
+ updating community integrations is handled by the
167
+ [UC Integration Manager](https://github.com/JackJPowell/uc-intg-manager); run
168
+ `npm run uc -- diagnose` after any integration update.
169
+
170
+ ## Review and apply
171
+
172
+ ```sh
173
+ npm run check:examples
174
+ npm run uc -- compile
175
+ npm run uc -- plan --out .uc/plan.json
176
+ ```
177
+
178
+ Typechecking checks source shape. Compile evaluates source and validates local
179
+ structure into `.uc/build.json`. Plan reads the remote and compares desired values,
180
+ live values, and the local baseline; it does not deploy changes. Inspect all
181
+ operations, deferred work, and conflicts. Do not automatically resolve drift by
182
+ adding `--overwrite-drift`. Removing resources requires an intentional prune or
183
+ `state forget`; see [the CLI reference](cli.md) for supported removal behavior.
184
+
185
+ When deployment is authorized, apply the reviewed plan, then verify:
186
+
187
+ ```sh
188
+ npm run uc -- apply .uc/plan.json
189
+ npm run uc -- check
190
+ ```
191
+
192
+ After any source change, compile and plan again. Never hand-edit a plan or local
193
+ state to bypass validation. A plan with no changes needs no apply. An adoption-only
194
+ plan can use `apply .uc/plan.json --adopt-only` to record ownership locally while
195
+ refusing remote writes. `npm run build` builds the CLI itself; it is separate from
196
+ configuration compilation.
@@ -0,0 +1,24 @@
1
+ # Local state that git does not hold
2
+
3
+ Your remote's configuration and operating state are deliberately kept out of
4
+ this repository. A fresh clone contains the tool only.
5
+
6
+ | Path | Contents | Back up? |
7
+ | ---------------------- | ------------------------------------------------------------ | ------------------------- |
8
+ | `remote.config.ts` | Your desired configuration | Yes (private git repo) |
9
+ | `generated/devices.ts` | Entity/command bindings from `inventory --bindings` | Optional (regenerable) |
10
+ | `.uc/targets/` | Remote address, identity and firmware | Yes |
11
+ | `.uc/credentials.json` | API key (mode 0600) | No; re-run `auth` if lost |
12
+ | `.uc/state/` | Logical key → remote ID bindings and the last applied values | **Yes, critical** |
13
+ | `.uc/journals/` | Apply journals used by `resume` and `rollback` | Yes |
14
+ | `.uc/*.json` | Builds, plans, inventories | No |
15
+
16
+ If you lose `.uc/state`, the next plan becomes an adoption pass. Recreate it
17
+ with `import`, review the result, and then `apply --adopt-only`. Never apply a
18
+ plan that unexpectedly creates resources which already exist on the remote.
19
+
20
+ When you move to a new machine, copy `.uc/` across (e.g. `rsync -a old/.uc/ new/.uc/`)
21
+ rather than re-running `connect`/`auth`/`import`. A new session or new clone
22
+ should reuse existing state, not start over.
23
+
24
+ For the full first-run procedure see the README's "Setup (agent runbook)".
@@ -0,0 +1,312 @@
1
+ # Snippets: common tasks
2
+
3
+ Copy-paste recipes for the things people most often ask for. Each one ends with
4
+ the same validation loop:
5
+
6
+ ```sh
7
+ npm run uc -- compile && npm run uc -- plan --out .uc/plan.json
8
+ # review: only the intended resources may change
9
+ npm run uc -- apply .uc/plan.json && npm run uc -- check
10
+ ```
11
+
12
+ Ground rules (see [AGENTS.md](../AGENTS.md)):
13
+
14
+ - Get every entity ID and `cmd_id` from `generated/devices.ts` or a fresh
15
+ inventory. Never guess command names.
16
+ - A command used in an activity must belong to an entity in that activity's
17
+ `entity_ids`.
18
+ - Arrays (`entity_ids`, sequences, page `items`) are replaced whole. Keep the
19
+ existing entries and their order.
20
+ - Imported configs use native objects; new resources can use the helpers
21
+ (`activity()`, `bind()`, `page()`, ...). Both forms are valid together.
22
+
23
+ ## Finding things
24
+
25
+ ```sh
26
+ # Which entities exist and what commands do they accept?
27
+ grep -n 'name:' generated/devices.ts
28
+ grep -n 'Apple TV' generated/devices.ts
29
+
30
+ # Refresh bindings after adding devices/integrations (files are never overwritten)
31
+ npm run uc -- inventory --bindings .uc/devices-$(date +%F).ts
32
+
33
+ # Where is a resource defined?
34
+ grep -n '"activity\.' remote.config.ts | grep -v '\.button\.\|\.page\.' # activities
35
+ grep -n 'activity.watch_tv.button' remote.config.ts # its buttons
36
+ grep -n '<entity-id>' remote.config.ts # every use of an entity
37
+
38
+ # Raw API reads (redacted)
39
+ npm run uc -- api GET /activities
40
+ npm run uc -- api GET '/intg/instances/<integration_id>/entities?filter=ALL'
41
+ ```
42
+
43
+ Physical button names: `BACK HOME VOICE VOLUME_UP VOLUME_DOWN MUTE DPAD_UP
44
+ DPAD_DOWN DPAD_LEFT DPAD_RIGHT DPAD_MIDDLE CHANNEL_UP CHANNEL_DOWN PREV PLAY
45
+ NEXT POWER RECORD MENU STOP`. Check `npm run uc -- api GET /cfg/device/button_layout` for the exact list.
46
+
47
+ ## Remap a physical button in an activity
48
+
49
+ Imported form: change only `cmd_id` (and `entity_id` if needed).
50
+
51
+ ```ts
52
+ "activity.watch_kaleidescape.button.next_short_press": {
53
+ kind: "activityButton",
54
+ parent: { $ref: "activity.watch_kaleidescape" },
55
+ id: "NEXT/short_press",
56
+ data: {
57
+ short_press: {
58
+ entity_id: "kaleidescape.main.media_player.XXXX",
59
+ cmd_id: "media_player.next", // was media_player.fast_forward
60
+ params: {},
61
+ },
62
+ },
63
+ },
64
+ ```
65
+
66
+ New binding with the helper:
67
+
68
+ ```ts
69
+ import { bind, command, ref } from "uc-config";
70
+
71
+ "activity.watch_tv.button.menu": bind(
72
+ ref("activity.watch_tv"),
73
+ "MENU",
74
+ command(appleTv, "media_player.context_menu"),
75
+ ),
76
+ ```
77
+
78
+ ## Add a long-press action
79
+
80
+ ```ts
81
+ "activity.watch_tv.button.home_long": bind(
82
+ ref("activity.watch_tv"),
83
+ "HOME",
84
+ command(appleTv, "APP_SWITCHER"),
85
+ "long_press",
86
+ ),
87
+ ```
88
+
89
+ ## Route volume to the receiver in every activity
90
+
91
+ ```ts
92
+ const receiver = "onkyo_driver.main.media_player.XXXX";
93
+ const volumeButtons = (activityKey: string) => ({
94
+ [`${activityKey}.button.vol_up`]: bind(ref(activityKey), "VOLUME_UP", command(receiver, "media_player.volume_up")),
95
+ [`${activityKey}.button.vol_down`]: bind(ref(activityKey), "VOLUME_DOWN", command(receiver, "media_player.volume_down")),
96
+ [`${activityKey}.button.mute`]: bind(ref(activityKey), "MUTE", command(receiver, "media_player.mute_toggle")),
97
+ });
98
+
99
+ resources: {
100
+ ...volumeButtons("activity.watch_tv"),
101
+ ...volumeButtons("activity.play_ps5"),
102
+ }
103
+ ```
104
+
105
+ If an activity already has imported `VOLUME_UP/short_press` resources, edit
106
+ those instead. Two resources with the same parent and `id` conflict.
107
+
108
+ ## Add an entity to an activity
109
+
110
+ Append to `entity_ids` and keep the existing order. Required before any of its
111
+ commands can be used in that activity.
112
+
113
+ ```ts
114
+ options: {
115
+ entity_ids: [
116
+ "lgwebos_driver.main.media_player.XXXX",
117
+ "onkyo_driver.main.media_player.XXXX",
118
+ "kaleidescape.main.remote.XXXX", // added
119
+ ],
120
+ },
121
+ ```
122
+
123
+ ## Change an activity's power-on / power-off sequence
124
+
125
+ ```ts
126
+ sequences: {
127
+ on: [
128
+ { type: "command", command: { entity_id: tv, cmd_id: "media_player.on", params: {} } },
129
+ { type: "delay", delay: 2000 },
130
+ { type: "command", command: { entity_id: receiver, cmd_id: "media_player.select_source", params: { source: "BD/DVD" } } },
131
+ ],
132
+ off: [
133
+ { type: "command", command: { entity_id: receiver, cmd_id: "media_player.off", params: {} } },
134
+ { type: "command", command: { entity_id: tv, cmd_id: "media_player.off", params: {} } },
135
+ ],
136
+ },
137
+ ```
138
+
139
+ `select_source` values are entity-specific. Copy them from an existing sequence
140
+ or the entity's `attributes.source_list` (`npm run uc -- api GET /entities/<id>`).
141
+
142
+ ## Create a new activity
143
+
144
+ ```ts
145
+ import { activity, command, delay, ref, bind, page, button } from "uc-config";
146
+
147
+ "activity.movie_night": activity({
148
+ name: "Movie Night",
149
+ entities: [tv, receiver, player, lights],
150
+ on: [
151
+ command(lights, "light.off"),
152
+ command(tv, "media_player.on"),
153
+ delay(1500),
154
+ command(receiver, "media_player.on"),
155
+ ],
156
+ off: [command(receiver, "media_player.off"), command(tv, "media_player.off")],
157
+ }),
158
+ "activity.movie_night.button.play": bind(ref("activity.movie_night"), "PLAY", command(player, "media_player.play_pause")),
159
+ ```
160
+
161
+ New activities apply in two phases: the first apply creates the activity and
162
+ its entity membership, then **plan again** to apply buttons/pages (`deferred`
163
+ in the plan output tells you this).
164
+
165
+ ## Add a touchscreen page with buttons
166
+
167
+ ```ts
168
+ "activity.watch_tv.page.apps": page(ref("activity.watch_tv"), {
169
+ name: "Apps",
170
+ grid: { width: 4, height: 6 },
171
+ items: [
172
+ button({ label: "Netflix", at: [0, 0], size: [2, 1], command: command(tv, "NETFLIX") }),
173
+ button({ label: "Prime", at: [2, 0], size: [2, 1], command: command(tv, "AMAZON") }),
174
+ button({ label: "Fix Inputs", at: [1, 2], size: [2, 2], command: command(fixMacro, "macro.run") }),
175
+ ],
176
+ }),
177
+ ```
178
+
179
+ Items must fit within the grid and must not overlap. For an existing imported
180
+ page, edit its `items` array in place.
181
+
182
+ ## Create a macro ("fix inputs" style)
183
+
184
+ ```ts
185
+ import { macro, command, delay } from "uc-config";
186
+
187
+ "macro.fix_inputs": macro({
188
+ name: "Fix Inputs",
189
+ entities: [tv, receiver],
190
+ steps: [
191
+ command(tv, "media_player.select_source", { source: "HDMI 2" }),
192
+ delay(500),
193
+ command(receiver, "media_player.select_source", { source: "BD/DVD" }),
194
+ ],
195
+ }),
196
+ ```
197
+
198
+ Run it from a button or page with `command("<macro entity id>", "macro.run")`.
199
+ For an existing macro the entity ID is its `id` in `remote.config.ts`. For a
200
+ new one, apply first, then read it from `npm run uc -- api GET /macros`.
201
+
202
+ ## Touch slider target
203
+
204
+ ```ts
205
+ options: {
206
+ touch_slider: { enabled: true, target: { entity_id: "lutron_driver.main.light.XXXX" } },
207
+ },
208
+ ```
209
+
210
+ ## Rename an activity / page
211
+
212
+ Change the display name only; keep the resource key and `id`:
213
+
214
+ ```ts
215
+ data: { name: { en_US: "Watch Movies" } }, // was "Watch Kaleidescape"
216
+ ```
217
+
218
+ To rename the _logical key_ too, edit the key and every `ref()`/`$ref` to it,
219
+ then run `npm run uc -- state move OLD_KEY NEW_KEY`.
220
+
221
+ ## Device settings
222
+
223
+ ```ts
224
+ import { settings } from "uc-config";
225
+
226
+ "settings.display": settings("display", { brightness: 60, auto_brightness: true }),
227
+ "settings.haptic": settings("haptic", { enabled: true }),
228
+ ```
229
+
230
+ Only declared fields are owned. Sections: `device display button haptic
231
+ localization power_saving sound bt profile voice_control`.
232
+
233
+ ## Repair after an integration update (orphaned entities)
234
+
235
+ ```sh
236
+ npm run uc -- diagnose
237
+ ```
238
+
239
+ **Entity dropped but still offered** (diagnose prints `fix: Re-add ...`):
240
+
241
+ ```sh
242
+ npm run uc -- api POST '/intg/instances/<integration_id>/entities/<local_id>' --data '{}' --write
243
+ npm run uc -- diagnose # expect All clear
244
+ npm run uc -- plan # expect 0 operations
245
+ ```
246
+
247
+ **Entity re-keyed** (diagnose lists a `candidate`):
248
+
249
+ ```sh
250
+ npm run uc -- api POST '/intg/instances/<integration_id>/entities/<candidate_local_id>' --data '{}' --write
251
+ grep -n '<old-entity-id>' remote.config.ts # entity_ids, sequences, buttons, pages, macros
252
+ # replace every occurrence with the new ID, then:
253
+ npm run uc -- compile && npm run uc -- plan --out .uc/plan.json && npm run uc -- apply .uc/plan.json
254
+ npm run uc -- diagnose
255
+ ```
256
+
257
+ If the plan reports `Cannot verify command` for the swapped entity, the
258
+ activity's live `included_entities` still lists the old one. Apply the
259
+ `entity_ids` change first, then replan for the buttons and sequences.
260
+
261
+ **Drift from a driver renaming something** (`Drift at data.name`): copy the
262
+ live value (shown in the plan) into source, then:
263
+
264
+ ```sh
265
+ npm run uc -- state forget <key> && npm run uc -- state adopt <key> <native-id>
266
+ npm run uc -- compile && npm run uc -- plan --out .uc/plan.json
267
+ npm run uc -- apply .uc/plan.json --adopt-only
268
+ ```
269
+
270
+ ## Pull changes made in the web configurator
271
+
272
+ ```sh
273
+ npm run uc -- import --out .uc/imported-$(date +%F).config.ts
274
+ npm run uc -- compile --config .uc/imported-$(date +%F).config.ts --out .uc/imported-build.json
275
+ diff <(jq -S . .uc/build.json) <(jq -S . .uc/imported-build.json) | less
276
+ ```
277
+
278
+ Imported keys are generic (`activity3`). Match resources by `kind` + `id` and
279
+ copy the changed fields into `remote.config.ts`. Then `plan` should report
280
+ 0 operations.
281
+
282
+ ## Undo the last apply
283
+
284
+ ```sh
285
+ npm run uc -- rollback --out .uc/rollback.json # builds a compensating plan
286
+ # review it like any plan, then:
287
+ npm run uc -- apply .uc/rollback.json
288
+ ```
289
+
290
+ ## Back up everything (scheduled job friendly)
291
+
292
+ ```sh
293
+ D=backups/$(date +%F); mkdir -p "$D"
294
+ npm run -s uc -- inventory --out "$D/inventory.json" # redacted raw state
295
+ npm run -s uc -- import --out "$D/remote.config.ts" # readable snapshot
296
+ tar czf "$D/dotuc.tgz" --exclude credentials.json .uc # ownership state
297
+ curl -sf http://<REMOTE_IP>:9999/api/backups/download -o "$D/intg-manager.json" # Integration Manager
298
+ ```
299
+
300
+ `npm run uc -- backup --out PRIVATE_PATH` produces the remote's native full
301
+ backup, but it **temporarily stops integrations and docks**, so don't schedule it.
302
+
303
+ ## Nightly health check (for an agent cron job)
304
+
305
+ ```sh
306
+ npm run -s uc -- diagnose --json > .uc/diagnose.json; d=$?
307
+ npm run -s uc -- plan --out .uc/plan-nightly.json; p=$?
308
+ echo "diagnose=$d plan=$p" # 0/0 = healthy; 2 = needs attention
309
+ ```
310
+
311
+ Report findings, and fix only the clear-cut cases (re-add a dropped entity that
312
+ is still offered). Escalate everything else to the user.