uc-config 0.3.0 → 0.3.1
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 +51 -7
- package/dist/client.d.ts +5 -0
- package/dist/client.js +10 -1
- package/dist/init.js +22 -0
- package/docs/cli.md +1 -1
- package/docs/snippets.md +15 -8
- 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.1
|
|
13
|
+
|
|
14
|
+
Action required: optional, run `npx uc-config init --refresh-docs` to get the
|
|
15
|
+
backup instructions in AGENTS.md.
|
|
16
|
+
|
|
17
|
+
- `backup` takes no arguments now: it saves the remote's full native backup to
|
|
18
|
+
`backups/<model>-<timestamp>.tar` (extension from the remote's filename) and
|
|
19
|
+
never overwrites, so older backups are kept. `--out` still works.
|
|
20
|
+
- `backup --list` lists existing backups without contacting the remote.
|
|
21
|
+
`doctor` reports how many there are, or suggests making the first one.
|
|
22
|
+
- `backup` writes `backups/.gitignore` so archives (unencrypted, with
|
|
23
|
+
integration credentials) stay out of git even in existing workspaces. New
|
|
24
|
+
workspaces also ignore `backups/` in the top-level `.gitignore`.
|
|
25
|
+
- AGENTS.md: agents offer a first backup when none exists, always ask before
|
|
26
|
+
running one, and never restore.
|
|
27
|
+
|
|
12
28
|
## 0.3.0
|
|
13
29
|
|
|
14
30
|
Action required: run `npx uc-config init --refresh-docs`. The agent
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { rollbackPlan } from "./recovery.js";
|
|
|
17
17
|
import { validateRequest } from "./schema.js";
|
|
18
18
|
import { diagnose, formatDiagnosis } from "./diagnose.js";
|
|
19
19
|
import { init } from "./init.js";
|
|
20
|
-
import { readFileSync, readdirSync } from "node:fs";
|
|
20
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
21
21
|
const pkgVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
22
22
|
const program = new Command()
|
|
23
23
|
.name("uc-config")
|
|
@@ -267,6 +267,10 @@ program
|
|
|
267
267
|
process.exitCode = 1;
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
+
const backups = listBackups(root());
|
|
271
|
+
console.log(backups.length
|
|
272
|
+
? `Full backups: ${backups.length} (latest ${backups.at(-1).file})`
|
|
273
|
+
: "No full backup yet. Offer the user one: npx uc-config backup");
|
|
270
274
|
});
|
|
271
275
|
program
|
|
272
276
|
.command("inventory")
|
|
@@ -602,20 +606,60 @@ program
|
|
|
602
606
|
});
|
|
603
607
|
program
|
|
604
608
|
.command("backup")
|
|
609
|
+
.description("Full native backup of the remote into backups/ (timestamped; never overwrites). Briefly stops integrations and docks.")
|
|
605
610
|
.option("--target <name>", "target", defaultTarget)
|
|
606
|
-
.
|
|
611
|
+
.option("--out <file>", "archive path (default: backups/<timestamp>)")
|
|
612
|
+
.option("--list", "list existing backups; no remote access")
|
|
607
613
|
.action(async (o) => {
|
|
614
|
+
if (o.list) {
|
|
615
|
+
const list = listBackups(root());
|
|
616
|
+
console.log(list.length
|
|
617
|
+
? list.map((b) => `${b.file} ${b.size}`).join("\n")
|
|
618
|
+
: "No full backups in backups/. Run: npx uc-config backup");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
608
621
|
await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
|
|
609
622
|
const { client, target } = await load(o.target);
|
|
610
623
|
await client.verifyTarget(target);
|
|
611
|
-
console.log("Exporting
|
|
612
|
-
const bytes = await client.
|
|
613
|
-
const
|
|
614
|
-
|
|
624
|
+
console.log("Exporting full backup: the remote stops integrations and docks for a few seconds, then restarts them.");
|
|
625
|
+
const { bytes, filename } = await client.downloadFile("/system/backup/export");
|
|
626
|
+
const ext = /\.(tar\.gz|tgz|tar|zip)$/i.exec(filename ?? "")?.[0] ?? ".tar";
|
|
627
|
+
const stamp = new Date()
|
|
628
|
+
.toISOString()
|
|
629
|
+
.replace(/:/g, "")
|
|
630
|
+
.replace(/\..+/, "");
|
|
631
|
+
const out = o.out ??
|
|
632
|
+
join(BACKUP_DIR, `${target.version.model.toLowerCase()}-${stamp}${ext}`);
|
|
633
|
+
const file = resolve(root(), out);
|
|
634
|
+
await mkdir(resolve(file, ".."), { recursive: true, mode: 0o700 });
|
|
635
|
+
// The archive holds integration credentials: keep backups/ out of git.
|
|
636
|
+
if (resolve(file, "..") === resolve(root(), BACKUP_DIR))
|
|
637
|
+
await writeFile(resolve(root(), BACKUP_DIR, ".gitignore"), "# Full remote backups contain credentials. Never commit them.\n*\n");
|
|
615
638
|
await writeFile(file, bytes, { mode: 0o600, flag: "wx" });
|
|
616
|
-
console.log(`Backup saved to ${
|
|
639
|
+
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.`);
|
|
617
640
|
});
|
|
618
641
|
});
|
|
642
|
+
const BACKUP_DIR = "backups";
|
|
643
|
+
function formatSize(n) {
|
|
644
|
+
return n > 1048576
|
|
645
|
+
? `${(n / 1048576).toFixed(1)} MB`
|
|
646
|
+
: `${Math.ceil(n / 1024)} KB`;
|
|
647
|
+
}
|
|
648
|
+
function listBackups(dir) {
|
|
649
|
+
try {
|
|
650
|
+
const d = join(dir, BACKUP_DIR);
|
|
651
|
+
return readdirSync(d)
|
|
652
|
+
.filter((f) => /\.(tar\.gz|tgz|tar|zip)$/i.test(f))
|
|
653
|
+
.sort()
|
|
654
|
+
.map((f) => ({
|
|
655
|
+
file: join(BACKUP_DIR, f),
|
|
656
|
+
size: formatSize(statSync(join(d, f)).size),
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
619
663
|
const ir = program.command("ir");
|
|
620
664
|
ir.command("learn <emitterId>")
|
|
621
665
|
.option("--target <name>", "target", defaultTarget)
|
package/dist/client.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export declare class CoreClient {
|
|
|
19
19
|
maybe(path: string): Promise<ObjectValue | null>;
|
|
20
20
|
list(path: string): Promise<ObjectValue[]>;
|
|
21
21
|
download(path: string): Promise<Uint8Array>;
|
|
22
|
+
/** Bytes plus the server-suggested filename (Content-Disposition), if any. */
|
|
23
|
+
downloadFile(path: string): Promise<{
|
|
24
|
+
bytes: Uint8Array;
|
|
25
|
+
filename?: string;
|
|
26
|
+
}>;
|
|
22
27
|
version(): Promise<Version>;
|
|
23
28
|
verifyTarget(target: Target): Promise<void>;
|
|
24
29
|
}
|
package/dist/client.js
CHANGED
|
@@ -122,6 +122,10 @@ export class CoreClient {
|
|
|
122
122
|
throw new Error(`${path}: pagination limit exceeded`);
|
|
123
123
|
}
|
|
124
124
|
async download(path) {
|
|
125
|
+
return (await this.downloadFile(path)).bytes;
|
|
126
|
+
}
|
|
127
|
+
/** Bytes plus the server-suggested filename (Content-Disposition), if any. */
|
|
128
|
+
async downloadFile(path) {
|
|
125
129
|
if (!path.startsWith("/") || path.startsWith("//") || path.includes(".."))
|
|
126
130
|
throw new Error("Invalid API path");
|
|
127
131
|
const response = await this.transport(new URL(path.slice(1), this.base), {
|
|
@@ -131,7 +135,12 @@ export class CoreClient {
|
|
|
131
135
|
});
|
|
132
136
|
if (!response.ok)
|
|
133
137
|
throw new ApiError(response.status, "GET", path);
|
|
134
|
-
|
|
138
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
139
|
+
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
|
|
140
|
+
return {
|
|
141
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
142
|
+
...(m ? { filename: decodeURIComponent(m[1]) } : {}),
|
|
143
|
+
};
|
|
135
144
|
}
|
|
136
145
|
async version() {
|
|
137
146
|
return this.get("/pub/version");
|
package/dist/init.js
CHANGED
|
@@ -45,6 +45,8 @@ exists, and skip steps that are done:
|
|
|
45
45
|
never writes to the remote.
|
|
46
46
|
5. \`npx uc-config compile && npx uc-config check\` (must be 0 operations).
|
|
47
47
|
6. \`git init && git add -A && git commit -m "Import Remote 3 config"\`
|
|
48
|
+
7. If \`npx uc-config backup --list\` shows no backups, offer the user a full
|
|
49
|
+
backup (see "Full backup" below). Ask first; don't run it unprompted.
|
|
48
50
|
|
|
49
51
|
If \`.uc/\` already exists, reuse it. Don't re-auth. If remote.config.ts or
|
|
50
52
|
\`.uc/state\` is lost, the remote still has everything: run sync again.
|
|
@@ -73,6 +75,24 @@ The remote is the source of truth; this folder is a working copy of it.
|
|
|
73
75
|
- Never use --overwrite-drift or --prune without the user's say-so.
|
|
74
76
|
- Never put secrets in remote.config.ts or print .uc/credentials.json.
|
|
75
77
|
|
|
78
|
+
## Full backup
|
|
79
|
+
|
|
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.
|
|
84
|
+
|
|
85
|
+
- Whenever you work in this folder and \`backup --list\` (or \`doctor\`) shows
|
|
86
|
+
no backup, ask the user once whether they'd like one now.
|
|
87
|
+
- Always ask before running it, and tell the user first: integrations and docks
|
|
88
|
+
stop for a few seconds and the remote shouldn't be used meanwhile.
|
|
89
|
+
- Each run adds a new file; older backups are kept. \`backup --list\` lists them.
|
|
90
|
+
Only delete old ones when the user asks.
|
|
91
|
+
- Archives are unencrypted and contain integration credentials. backups/ is
|
|
92
|
+
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.
|
|
95
|
+
|
|
76
96
|
## When something is broken
|
|
77
97
|
|
|
78
98
|
Run \`npx uc-config diagnose\`. It is read-only and prints a \`fix:\` per issue.
|
|
@@ -126,6 +146,8 @@ const tsconfig = {
|
|
|
126
146
|
const gitignore = `node_modules/
|
|
127
147
|
# Credentials, state and journals. Back up .uc/state and .uc/journals privately.
|
|
128
148
|
.uc/
|
|
149
|
+
# Full remote backups: unencrypted, contain integration credentials.
|
|
150
|
+
backups/
|
|
129
151
|
.env
|
|
130
152
|
.env.*
|
|
131
153
|
`;
|
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`
|
|
33
|
+
| `backup [--out f] [--list]` | stops intgs | Full native backup to `backups/<timestamp>`; keeps older ones. Ask first. |
|
|
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
|
@@ -316,18 +316,25 @@ npm run uc -- rollback --out .uc/rollback.json # builds a compensating plan
|
|
|
316
316
|
npm run uc -- apply .uc/rollback.json
|
|
317
317
|
```
|
|
318
318
|
|
|
319
|
-
##
|
|
319
|
+
## Full remote backup
|
|
320
320
|
|
|
321
321
|
```sh
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
npm run -s uc -- import --out "$D/remote.config.ts" # readable snapshot
|
|
325
|
-
tar czf "$D/dotuc.tgz" --exclude credentials.json .uc # ownership state
|
|
326
|
-
curl -sf http://<REMOTE_IP>:9999/api/backups/download -o "$D/intg-manager.json" # Integration Manager
|
|
322
|
+
npx uc-config backup --list # existing backups (no remote access)
|
|
323
|
+
npx uc-config backup # new backups/<model>-<timestamp>.tar
|
|
327
324
|
```
|
|
328
325
|
|
|
329
|
-
|
|
330
|
-
|
|
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.
|
|
330
|
+
|
|
331
|
+
- It **stops integrations and docks for a few seconds**. Ask the user first,
|
|
332
|
+
and don't schedule it.
|
|
333
|
+
- The archive is unencrypted and contains integration credentials.
|
|
334
|
+
`backups/` is gitignored (and gets its own `.gitignore`); keep it private.
|
|
335
|
+
- It does not include Wi-Fi settings, the admin or web-configurator PIN, or
|
|
336
|
+
API keys, so `npx uc-config auth` is needed again after a restore.
|
|
337
|
+
- Restore it in the web configurator. uc-config never restores.
|
|
331
338
|
|
|
332
339
|
## Nightly health check (for an agent cron job)
|
|
333
340
|
|