uc-config 0.2.2 → 0.2.4
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/CHANGELOG.md +20 -0
- package/README.md +22 -8
- package/dist/inventory.d.ts +2 -0
- package/dist/inventory.js +36 -11
- package/docs/snippets.md +7 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,26 @@ npx uc-config init --refresh-docs
|
|
|
9
9
|
npx uc-config compile && npx uc-config plan # must show 0 operations
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## 0.2.4
|
|
13
|
+
|
|
14
|
+
Action required: none. Existing configs keep their keys.
|
|
15
|
+
|
|
16
|
+
- `import` builds readable keys from display names (`activity.play_ps5`,
|
|
17
|
+
`activity.play_ps5.button.mute_short_press`, `entity.living_room_lamp`)
|
|
18
|
+
instead of `activity8`. Duplicate names get `_2`, `_3`. To rename keys in an
|
|
19
|
+
existing config, use `state move OLD NEW`.
|
|
20
|
+
|
|
21
|
+
## 0.2.3
|
|
22
|
+
|
|
23
|
+
Action required: none. Docs only.
|
|
24
|
+
|
|
25
|
+
- README: what to do when a plan contains operations you didn't make, how to
|
|
26
|
+
read full sequence changes from `.uc/plan.json`, telling a sleeping remote
|
|
27
|
+
from a machine without LAN access, and what `deferred` means.
|
|
28
|
+
- Snippets: corrected the entity-swap recipe. Swapped-in commands are deferred
|
|
29
|
+
until the `entity_ids` change is applied, rather than failing with
|
|
30
|
+
`Cannot verify command`.
|
|
31
|
+
|
|
12
32
|
## 0.2.2
|
|
13
33
|
|
|
14
34
|
Action required: optional. Run `npx uc-config init --refresh-docs` to get the
|
package/README.md
CHANGED
|
@@ -208,6 +208,19 @@ npm run uc -- check # must report 0 operations
|
|
|
208
208
|
remote identity, firmware and preconditions first. A plan with zero operations
|
|
209
209
|
needs no apply. See [docs/snippets.md](docs/snippets.md) for common edits.
|
|
210
210
|
|
|
211
|
+
**If the plan touches things you didn't edit, stop.** Those operations are
|
|
212
|
+
earlier source changes that were never applied, or edits made on the remote
|
|
213
|
+
since. Don't apply them along with your change: sync with the live remote first
|
|
214
|
+
(see [Pull changes made in the web configurator](docs/snippets.md#pull-changes-made-in-the-web-configurator)),
|
|
215
|
+
then replan until only your change is left.
|
|
216
|
+
|
|
217
|
+
**The plan output shortens long sequences.** To see the exact step-by-step
|
|
218
|
+
change, compare each operation's `before` with its `desired` in `.uc/plan.json`:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
jq '.operations[] | {key, action, before, desired}' .uc/plan.json
|
|
222
|
+
```
|
|
223
|
+
|
|
211
224
|
## Diagnostics and fixing problems
|
|
212
225
|
|
|
213
226
|
Run `diagnose` whenever something looks wrong on the remote, after any
|
|
@@ -235,14 +248,15 @@ For each orphan it proposes a fix:
|
|
|
235
248
|
|
|
236
249
|
Plan/apply problems:
|
|
237
250
|
|
|
238
|
-
| Message
|
|
239
|
-
|
|
|
240
|
-
| `Drift at <fields>`
|
|
241
|
-
| `Managed resource disappeared`
|
|
242
|
-
| `Cannot verify command`
|
|
243
|
-
| `Remote firmware changed; reconnect and replan`
|
|
244
|
-
| `transport failed` / timeouts
|
|
245
|
-
|
|
|
251
|
+
| Message | Meaning and fix |
|
|
252
|
+
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
253
|
+
| `Drift at <fields>` | Someone (or a driver update) changed an owned field on the remote. Copy the live value into source, or rerun `plan --overwrite-drift` only if the user wants the source value restored. |
|
|
254
|
+
| `Managed resource disappeared` | Resource was deleted on the remote. Run `diagnose`; re-add or remove from source. |
|
|
255
|
+
| `Cannot verify command` | `cmd_id` isn't advertised by that entity, or the entity isn't in the activity's `entity_ids`. Check `generated/devices.ts`, refresh inventory. |
|
|
256
|
+
| `Remote firmware changed; reconnect and replan` | Remote auto-updated. Rerun `connect <name> --host ...` (keeps credentials), then plan. |
|
|
257
|
+
| `transport failed` / timeouts | Remote asleep, or this machine has no LAN access. Ping the router and another LAN device: if they fail too, it's this machine (on macOS, Local Network privacy), and waking the remote won't help. |
|
|
258
|
+
| `deferred ...: apply prerequisites, then replan` | The item depends on another change in the same plan (e.g. a new entity in `entity_ids`). Apply the plan, then plan again for the deferred items. |
|
|
259
|
+
| Uncertain writes after a crash | Run `resume`. Never blindly rerun apply; use `state adopt KEY ID` if a create actually succeeded. |
|
|
246
260
|
|
|
247
261
|
`npm run uc -- api GET <path>` makes a raw authenticated Core API read (output
|
|
248
262
|
redacted). Non-GET methods require `--write` and should only be used for the
|
package/dist/inventory.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface Inventory {
|
|
|
7
7
|
unsupported: string[];
|
|
8
8
|
}
|
|
9
9
|
export declare function inventory(client: CoreClient): Promise<Inventory>;
|
|
10
|
+
/** snake_case key segment; falls back to the id when the name has no letters/digits. */
|
|
11
|
+
export declare function slug(name: string | undefined, fallback: string): string;
|
|
10
12
|
export declare function importConfig(client: CoreClient): Promise<{
|
|
11
13
|
config: Config;
|
|
12
14
|
warnings: string[];
|
package/dist/inventory.js
CHANGED
|
@@ -58,13 +58,38 @@ export async function inventory(client) {
|
|
|
58
58
|
}
|
|
59
59
|
return result;
|
|
60
60
|
}
|
|
61
|
+
/** Display name from a string or a localized {en: ...} map. */
|
|
62
|
+
function nameOf(item) {
|
|
63
|
+
const n = isObject(item) ? item.name : undefined;
|
|
64
|
+
if (typeof n === "string")
|
|
65
|
+
return n;
|
|
66
|
+
if (isObject(n)) {
|
|
67
|
+
const v = n.en ?? Object.values(n).find((x) => typeof x === "string");
|
|
68
|
+
if (typeof v === "string")
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/** snake_case key segment; falls back to the id when the name has no letters/digits. */
|
|
74
|
+
export function slug(name, fallback) {
|
|
75
|
+
const s = (x) => x
|
|
76
|
+
.normalize("NFKD")
|
|
77
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
80
|
+
.replace(/^_+|_+$/g, "");
|
|
81
|
+
return s(name ?? "") || s(fallback) || "item";
|
|
82
|
+
}
|
|
61
83
|
export async function importConfig(client) {
|
|
62
84
|
const inv = await inventory(client);
|
|
63
85
|
const resources = {};
|
|
64
86
|
const warnings = [...inv.unsupported.map((p) => `Unsupported endpoint ${p}`)];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
// Readable keys from display names ("Play PS5" -> activity.play_ps5);
|
|
88
|
+
// collisions get _2, _3, ... in import order.
|
|
89
|
+
const add = (r, base) => {
|
|
90
|
+
let key = base;
|
|
91
|
+
for (let n = 2; key in resources; n++)
|
|
92
|
+
key = `${base}_${n}`;
|
|
68
93
|
resources[key] = r;
|
|
69
94
|
return key;
|
|
70
95
|
};
|
|
@@ -93,7 +118,7 @@ export async function importConfig(client) {
|
|
|
93
118
|
warnings.push(`${id}: secret-bearing settings require manual secret() references`);
|
|
94
119
|
continue;
|
|
95
120
|
}
|
|
96
|
-
const key = add(r);
|
|
121
|
+
const key = add(r, `${kind}.${slug(nameOf(item), id)}`);
|
|
97
122
|
if (kind === "integration" || kind === "dock")
|
|
98
123
|
warnings.push(`${key}: existing resource imported; supply create/setup inputs and secret references to reproduce on a new target`);
|
|
99
124
|
if (kind === "remote" && collection !== "externalRemotes")
|
|
@@ -112,7 +137,7 @@ export async function importConfig(client) {
|
|
|
112
137
|
parent: { $ref: key },
|
|
113
138
|
id: `${b.button}/${press}`,
|
|
114
139
|
data: { [press]: b[press] },
|
|
115
|
-
});
|
|
140
|
+
}, `${key}.button.${slug(String(b.button), "button")}_${press}`);
|
|
116
141
|
const pages = await client.list(`/${prefix}/${encodeURIComponent(id)}/ui/pages`);
|
|
117
142
|
for (const p of pages)
|
|
118
143
|
add({
|
|
@@ -120,7 +145,7 @@ export async function importConfig(client) {
|
|
|
120
145
|
parent: { $ref: key },
|
|
121
146
|
id: String(p.page_id),
|
|
122
147
|
data: writable(`/${prefix}/{entityId}/ui/pages/{pageId}`, "patch", p),
|
|
123
|
-
});
|
|
148
|
+
}, `${key}.page.${slug(nameOf(p), String(p.page_id))}`);
|
|
124
149
|
}
|
|
125
150
|
catch (e) {
|
|
126
151
|
if (e instanceof ApiError && [404, 405].includes(e.status))
|
|
@@ -139,7 +164,7 @@ export async function importConfig(client) {
|
|
|
139
164
|
parent: { $ref: key },
|
|
140
165
|
id: String(c[group ? "group_id" : "page_id"]),
|
|
141
166
|
data: writable(`/profiles/{profileId}/${child}/{${group ? "groupId" : "pageId"}}`, "patch", c),
|
|
142
|
-
});
|
|
167
|
+
}, `${key}.${group ? "group" : "page"}.${slug(nameOf(c), String(c[group ? "group_id" : "page_id"]))}`);
|
|
143
168
|
}
|
|
144
169
|
}
|
|
145
170
|
if (collection === "irRemotes") {
|
|
@@ -158,7 +183,7 @@ export async function importConfig(client) {
|
|
|
158
183
|
format: code.code.format,
|
|
159
184
|
value: code.code.value,
|
|
160
185
|
},
|
|
161
|
-
});
|
|
186
|
+
}, `${key}.ir.${slug(code.cmd_id, "code")}`);
|
|
162
187
|
}
|
|
163
188
|
}
|
|
164
189
|
}
|
|
@@ -172,7 +197,7 @@ export async function importConfig(client) {
|
|
|
172
197
|
id: String(item.entity_id),
|
|
173
198
|
parent: item.integration_id,
|
|
174
199
|
data: writable("/entities/{entityId}", "patch", item),
|
|
175
|
-
});
|
|
200
|
+
}, `entity.${slug(nameOf(item), String(item.entity_id))}`);
|
|
176
201
|
}
|
|
177
202
|
const usedDrivers = new Set((inv.collections.integrations ?? []).map((i) => i.driver_id));
|
|
178
203
|
for (const item of inv.collections.drivers ?? []) {
|
|
@@ -185,7 +210,7 @@ export async function importConfig(client) {
|
|
|
185
210
|
warnings.push(`${item.driver_id}: driver settings contain secrets; provide secret references manually`);
|
|
186
211
|
continue;
|
|
187
212
|
}
|
|
188
|
-
add({ kind: "driver", id: String(item.driver_id), data });
|
|
213
|
+
add({ kind: "driver", id: String(item.driver_id), data }, `driver.${slug(nameOf(item), String(item.driver_id))}`);
|
|
189
214
|
}
|
|
190
215
|
for (const [section, data] of Object.entries(inv.settings)) {
|
|
191
216
|
const picked = writable(`/cfg/${section}`, "patch", data);
|
|
@@ -198,7 +223,7 @@ export async function importConfig(client) {
|
|
|
198
223
|
if (!Object.keys(picked).length)
|
|
199
224
|
continue;
|
|
200
225
|
if (!secretFields(picked).length)
|
|
201
|
-
add({ kind: "settings", id: section, data: picked });
|
|
226
|
+
add({ kind: "settings", id: section, data: picked }, `settings.${section}`);
|
|
202
227
|
else
|
|
203
228
|
warnings.push(`${section}: secret-bearing settings omitted; configure secret references`);
|
|
204
229
|
}
|
package/docs/snippets.md
CHANGED
|
@@ -254,9 +254,10 @@ npm run uc -- compile && npm run uc -- plan --out .uc/plan.json && npm run uc --
|
|
|
254
254
|
npm run uc -- diagnose
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
activity's
|
|
259
|
-
`
|
|
257
|
+
Buttons and sequences that use the new entity show up as **deferred** until the
|
|
258
|
+
activity's `entity_ids` change is on the remote. Apply the plan, then plan and
|
|
259
|
+
apply again for the deferred items. `check` must report 0 operations at the
|
|
260
|
+
end.
|
|
260
261
|
|
|
261
262
|
**Drift from a driver renaming something** (`Drift at data.name`): copy the
|
|
262
263
|
live value (shown in the plan) into source, then:
|
|
@@ -275,8 +276,9 @@ npm run uc -- compile --config .uc/imported-$(date +%F).config.ts --out .uc/impo
|
|
|
275
276
|
diff <(jq -S . .uc/build.json) <(jq -S . .uc/imported-build.json) | less
|
|
276
277
|
```
|
|
277
278
|
|
|
278
|
-
Imported keys
|
|
279
|
-
|
|
279
|
+
Imported keys come from display names (`activity.watch_tv`), so they usually
|
|
280
|
+
match yours, but a renamed activity gets a different key. Match resources by
|
|
281
|
+
`kind` + `id` and copy the changed fields into `remote.config.ts`. Then `plan` should report
|
|
280
282
|
0 operations.
|
|
281
283
|
|
|
282
284
|
## Undo the last apply
|