uc-config 0.3.1 → 0.3.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/CHANGELOG.md +16 -0
- package/dist/cli.js +40 -5
- package/dist/init.js +14 -5
- package/dist/util.d.ts +2 -0
- package/dist/util.js +11 -0
- package/docs/cli.md +1 -1
- package/docs/snippets.md +16 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,22 @@ 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.3.2
|
|
13
|
+
|
|
14
|
+
Action required: optional, run `npx uc-config init --refresh-docs`. Backups
|
|
15
|
+
made with 0.3.1 are zip files saved as `.tar`; rename them to `.zip`.
|
|
16
|
+
|
|
17
|
+
- `backup` detects the archive type from its contents. The remote sends no
|
|
18
|
+
filename and the archive is a zip, so 0.3.1 saved it with a `.tar` extension.
|
|
19
|
+
- `backup` also saves the UC Integration Manager's export beside the archive
|
|
20
|
+
(`<name>-intg-manager.json`). The native backup doesn't hold the setup of
|
|
21
|
+
community integrations (Onkyo, Oppo, Kaleidescape, ...); the manager's export
|
|
22
|
+
does. Default `http://<remote>:9999`; `--intg-manager <url>` sets and
|
|
23
|
+
remembers another address; `--no-intg-manager` skips it. A missing manager
|
|
24
|
+
never fails the backup.
|
|
25
|
+
- Docs and AGENTS.md no longer claim the native backup covers all integration
|
|
26
|
+
settings.
|
|
27
|
+
|
|
12
28
|
## 0.3.1
|
|
13
29
|
|
|
14
30
|
Action required: optional, run `npx uc-config init --refresh-docs` to get the
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { writeFile, mkdir, readFile, rename } from "node:fs/promises";
|
|
4
4
|
import { syncConfig, parseSource, renderSource, formatSync, remoteGone, } from "./sync.js";
|
|
5
|
-
import { resolve, join } from "node:path";
|
|
5
|
+
import { resolve, join, relative } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { Writable } from "node:stream";
|
|
8
8
|
import { ApiError, CoreClient, resolveSecrets } from "./client.js";
|
|
@@ -12,7 +12,7 @@ import { Engine, PendingSetup } from "./engine.js";
|
|
|
12
12
|
import { formatPlan, makePlan } from "./planner.js";
|
|
13
13
|
import { inventory, importConfig, generateBindings } from "./inventory.js";
|
|
14
14
|
import { emptyState, } from "./model.js";
|
|
15
|
-
import { readJson, saveJson, redact, withLock, isObject } from "./util.js";
|
|
15
|
+
import { readJson, saveJson, redact, withLock, isObject, archiveExtension, } from "./util.js";
|
|
16
16
|
import { rollbackPlan } from "./recovery.js";
|
|
17
17
|
import { validateRequest } from "./schema.js";
|
|
18
18
|
import { diagnose, formatDiagnosis } from "./diagnose.js";
|
|
@@ -267,7 +267,7 @@ program
|
|
|
267
267
|
process.exitCode = 1;
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
-
const backups = listBackups(root());
|
|
270
|
+
const backups = listBackups(root()).filter((b) => !b.file.endsWith("-intg-manager.json"));
|
|
271
271
|
console.log(backups.length
|
|
272
272
|
? `Full backups: ${backups.length} (latest ${backups.at(-1).file})`
|
|
273
273
|
: "No full backup yet. Offer the user one: npx uc-config backup");
|
|
@@ -610,6 +610,8 @@ program
|
|
|
610
610
|
.option("--target <name>", "target", defaultTarget)
|
|
611
611
|
.option("--out <file>", "archive path (default: backups/<timestamp>)")
|
|
612
612
|
.option("--list", "list existing backups; no remote access")
|
|
613
|
+
.option("--intg-manager <url>", "Integration Manager base URL (remembered); default http://<remote>:9999")
|
|
614
|
+
.option("--no-intg-manager", "skip the Integration Manager backup")
|
|
613
615
|
.action(async (o) => {
|
|
614
616
|
if (o.list) {
|
|
615
617
|
const list = listBackups(root());
|
|
@@ -623,7 +625,7 @@ program
|
|
|
623
625
|
await client.verifyTarget(target);
|
|
624
626
|
console.log("Exporting full backup: the remote stops integrations and docks for a few seconds, then restarts them.");
|
|
625
627
|
const { bytes, filename } = await client.downloadFile("/system/backup/export");
|
|
626
|
-
const ext =
|
|
628
|
+
const ext = archiveExtension(bytes, filename);
|
|
627
629
|
const stamp = new Date()
|
|
628
630
|
.toISOString()
|
|
629
631
|
.replace(/:/g, "")
|
|
@@ -637,6 +639,39 @@ program
|
|
|
637
639
|
await writeFile(resolve(root(), BACKUP_DIR, ".gitignore"), "# Full remote backups contain credentials. Never commit them.\n*\n");
|
|
638
640
|
await writeFile(file, bytes, { mode: 0o600, flag: "wx" });
|
|
639
641
|
console.log(`Backup saved to ${out} (${formatSize(bytes.length)}). It is unencrypted and contains integration credentials: keep it private. Restore it from the web configurator.`);
|
|
642
|
+
if (o.intgManager === false)
|
|
643
|
+
return;
|
|
644
|
+
// Community integrations keep their setup data outside the native backup;
|
|
645
|
+
// the Integration Manager's export is what holds it.
|
|
646
|
+
const saved = local("intg-manager.json");
|
|
647
|
+
if (typeof o.intgManager === "string")
|
|
648
|
+
await saveJson(saved, { url: new URL(o.intgManager).origin });
|
|
649
|
+
let base = `${new URL(target.host).protocol}//${new URL(target.host).hostname}:9999`;
|
|
650
|
+
try {
|
|
651
|
+
base = (await readJson(saved)).url;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
/* default */
|
|
655
|
+
}
|
|
656
|
+
const companion = file.replace(/(\.tar\.gz|\.[a-z0-9]+)$/i, "") + "-intg-manager.json";
|
|
657
|
+
try {
|
|
658
|
+
const res = await fetch(`${base}/api/v1/backups/export`, {
|
|
659
|
+
signal: AbortSignal.timeout(60000),
|
|
660
|
+
redirect: "error",
|
|
661
|
+
});
|
|
662
|
+
const body = await res.text();
|
|
663
|
+
const json = res.ok ? JSON.parse(body) : undefined;
|
|
664
|
+
if (!isObject(json) || !isObject(json.remotes))
|
|
665
|
+
throw new Error(`HTTP ${res.status}, not a manager backup`);
|
|
666
|
+
if (!Object.keys(json.remotes).some((k) => k.replace(/_/g, ":").toUpperCase() ===
|
|
667
|
+
target.identity.toUpperCase()))
|
|
668
|
+
console.log("Note: the Integration Manager backup has no entry for this remote.");
|
|
669
|
+
await writeFile(companion, body, { mode: 0o600, flag: "wx" });
|
|
670
|
+
console.log(`Integration Manager backup saved to ${relative(root(), companion)} (community integration settings; restore it in the Integration Manager).`);
|
|
671
|
+
}
|
|
672
|
+
catch (e) {
|
|
673
|
+
console.log(`No Integration Manager backup (${base}: ${e.message}). If you use community integrations and the manager runs elsewhere, rerun with --intg-manager http://<host>:9999.`);
|
|
674
|
+
}
|
|
640
675
|
});
|
|
641
676
|
});
|
|
642
677
|
const BACKUP_DIR = "backups";
|
|
@@ -649,7 +684,7 @@ function listBackups(dir) {
|
|
|
649
684
|
try {
|
|
650
685
|
const d = join(dir, BACKUP_DIR);
|
|
651
686
|
return readdirSync(d)
|
|
652
|
-
.filter((f) => /\.(tar\.gz|tgz|tar|zip)$/i.test(f))
|
|
687
|
+
.filter((f) => /\.(tar\.gz|tgz|tar|zip|bin)$|-intg-manager\.json$/i.test(f))
|
|
653
688
|
.sort()
|
|
654
689
|
.map((f) => ({
|
|
655
690
|
file: join(BACKUP_DIR, f),
|
package/dist/init.js
CHANGED
|
@@ -78,9 +78,17 @@ The remote is the source of truth; this folder is a working copy of it.
|
|
|
78
78
|
## Full backup
|
|
79
79
|
|
|
80
80
|
\`npx uc-config backup\` saves the remote's own full backup (the same archive
|
|
81
|
-
as the web configurator's backup page) to \`backups/<model>-<timestamp>.
|
|
82
|
-
It covers activities, macros, pages,
|
|
83
|
-
|
|
81
|
+
as the web configurator's backup page) to \`backups/<model>-<timestamp>.zip\`.
|
|
82
|
+
It covers activities, macros, pages, profiles, icons, IR codes and the
|
|
83
|
+
built-in integrations' settings, and is what you restore after a factory reset.
|
|
84
|
+
|
|
85
|
+
It does **not** hold the setup of community integrations installed through the
|
|
86
|
+
UC Integration Manager. When the manager is reachable (default
|
|
87
|
+
\`http://<remote>:9999\`; pass \`--intg-manager <url>\` once if it runs
|
|
88
|
+
elsewhere), \`backup\` also saves its export beside the archive as
|
|
89
|
+
\`<name>-intg-manager.json\`. If it reports no manager backup and the user has
|
|
90
|
+
community integrations, tell them those integrations would need setting up
|
|
91
|
+
again after a restore.
|
|
84
92
|
|
|
85
93
|
- Whenever you work in this folder and \`backup --list\` (or \`doctor\`) shows
|
|
86
94
|
no backup, ask the user once whether they'd like one now.
|
|
@@ -90,8 +98,9 @@ and IR codes, and is what you restore after a factory reset.
|
|
|
90
98
|
Only delete old ones when the user asks.
|
|
91
99
|
- Archives are unencrypted and contain integration credentials. backups/ is
|
|
92
100
|
gitignored: never commit, upload or print them.
|
|
93
|
-
- Restoring replaces the remote's configuration: the user
|
|
94
|
-
configurator
|
|
101
|
+
- Restoring replaces the remote's configuration: the user restores the archive
|
|
102
|
+
in the web configurator, then the manager file in the Integration Manager.
|
|
103
|
+
Never restore from here.
|
|
95
104
|
|
|
96
105
|
## When something is broken
|
|
97
106
|
|
package/dist/util.d.ts
CHANGED
|
@@ -15,4 +15,6 @@ export declare function withLock<T>(file: string, fn: () => Promise<T>): Promise
|
|
|
15
15
|
export declare function resolveRefs<T>(value: T, state: State): T;
|
|
16
16
|
export declare function references(value: unknown): string[];
|
|
17
17
|
export declare function redact(value: unknown): unknown;
|
|
18
|
+
/** Archive type from its magic bytes; the remote may not send a filename. */
|
|
19
|
+
export declare function archiveExtension(bytes: Uint8Array, filename?: string): string;
|
|
18
20
|
export declare function secretFields(v: unknown, path?: string[]): string[];
|
package/dist/util.js
CHANGED
|
@@ -129,6 +129,17 @@ export function redact(value) {
|
|
|
129
129
|
sensitive.test(k) ? "[REDACTED]" : redact(v),
|
|
130
130
|
]));
|
|
131
131
|
}
|
|
132
|
+
/** Archive type from its magic bytes; the remote may not send a filename. */
|
|
133
|
+
export function archiveExtension(bytes, filename) {
|
|
134
|
+
if (bytes[0] === 0x50 && bytes[1] === 0x4b)
|
|
135
|
+
return ".zip";
|
|
136
|
+
if (bytes[0] === 0x1f && bytes[1] === 0x8b)
|
|
137
|
+
return ".tar.gz";
|
|
138
|
+
if (bytes.length > 262 &&
|
|
139
|
+
String.fromCharCode(...bytes.slice(257, 262)) === "ustar")
|
|
140
|
+
return ".tar";
|
|
141
|
+
return /\.(tar\.gz|tgz|tar|zip)$/i.exec(filename ?? "")?.[0] ?? ".bin";
|
|
142
|
+
}
|
|
132
143
|
export function secretFields(v, path = []) {
|
|
133
144
|
if (!isObject(v) && !Array.isArray(v))
|
|
134
145
|
return [];
|
package/docs/cli.md
CHANGED
|
@@ -30,7 +30,7 @@ the workspace's only target if exactly one is connected; otherwise `home`.
|
|
|
30
30
|
| `ir learn/capture <emitterId>` | yes | Learn IR codes from a physical remote. |
|
|
31
31
|
| `state adopt <key> <id>` / `forget` / `move` | no | Edit local ownership bindings. |
|
|
32
32
|
| `rollback [--out f]` | no | Build a compensating plan from the last journal. |
|
|
33
|
-
| `backup [--out f] [--list]`
|
|
33
|
+
| `backup [--out f] [--list] [--intg-manager u]` | stops intgs | Native backup + Integration Manager export to `backups/`; keeps old ones. |
|
|
34
34
|
| `api <METHOD> <path> [--data json] [--write]` | only with --write | Raw authenticated Core API call; output redacted. |
|
|
35
35
|
|
|
36
36
|
Exit codes: `0` ok, `1` error, `2` drift/conflicts/deferred/diagnose findings,
|
package/docs/snippets.md
CHANGED
|
@@ -320,13 +320,23 @@ npm run uc -- apply .uc/rollback.json
|
|
|
320
320
|
|
|
321
321
|
```sh
|
|
322
322
|
npx uc-config backup --list # existing backups (no remote access)
|
|
323
|
-
npx uc-config backup # new backups/<model>-<timestamp>.
|
|
323
|
+
npx uc-config backup # new backups/<model>-<timestamp>.zip
|
|
324
324
|
```
|
|
325
325
|
|
|
326
326
|
This is the remote's own full backup, the same archive the web configurator
|
|
327
|
-
downloads: activities, macros, pages,
|
|
328
|
-
|
|
329
|
-
saves somewhere else.
|
|
327
|
+
downloads: activities, macros, pages, profiles, icons, IR codes and the
|
|
328
|
+
built-in integrations' settings. Each run adds a file and keeps the older ones.
|
|
329
|
+
`--out <path>` saves somewhere else.
|
|
330
|
+
|
|
331
|
+
**Community integrations** (installed through the UC Integration Manager, e.g.
|
|
332
|
+
Onkyo, Oppo, Kaleidescape, Lutron) keep their setup data outside this archive.
|
|
333
|
+
`backup` therefore also downloads the manager's export
|
|
334
|
+
(`/api/v1/backups/export`) and saves it beside the archive as
|
|
335
|
+
`<name>-intg-manager.json`. It tries `http://<remote>:9999` by default; if the
|
|
336
|
+
manager runs elsewhere (Docker, another host), pass
|
|
337
|
+
`--intg-manager http://<host>:9999` once and it is remembered.
|
|
338
|
+
`--no-intg-manager` skips it. If the manager can't be reached, the native
|
|
339
|
+
backup is still saved and the command says so.
|
|
330
340
|
|
|
331
341
|
- It **stops integrations and docks for a few seconds**. Ask the user first,
|
|
332
342
|
and don't schedule it.
|
|
@@ -334,7 +344,8 @@ saves somewhere else.
|
|
|
334
344
|
`backups/` is gitignored (and gets its own `.gitignore`); keep it private.
|
|
335
345
|
- It does not include Wi-Fi settings, the admin or web-configurator PIN, or
|
|
336
346
|
API keys, so `npx uc-config auth` is needed again after a restore.
|
|
337
|
-
- Restore
|
|
347
|
+
- Restore the archive in the web configurator, then the `-intg-manager.json`
|
|
348
|
+
file in the Integration Manager. uc-config never restores.
|
|
338
349
|
|
|
339
350
|
## Nightly health check (for an agent cron job)
|
|
340
351
|
|