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 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 = /\.(tar\.gz|tgz|tar|zip)$/i.exec(filename ?? "")?.[0] ?? ".tar";
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>.tar\`.
82
- It covers activities, macros, pages, integrations and their settings, icons
83
- and IR codes, and is what you restore after a factory reset.
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 does that in the web
94
- configurator. Never restore from here.
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]` | stops intgs | Full native backup to `backups/<timestamp>`; keeps older ones. Ask first. |
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>.tar
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, integrations and their settings, icons
328
- and IR codes. Each run adds a file and keeps the older ones. `--out <path>`
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 it in the web configurator. uc-config never restores.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uc-config",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Configuration-as-code for the Unfolded Circle Remote 3, designed to be driven by coding agents",
6
6
  "license": "MIT",