uc-config 0.1.0 → 0.2.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/README.md +58 -8
- package/dist/cli.js +126 -31
- package/dist/init.js +20 -4
- package/docs/cli.md +25 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,24 +16,71 @@ hardware. Humans can use it directly too.
|
|
|
16
16
|
> for copy-paste recipes. Ask the user only for what you cannot discover:
|
|
17
17
|
> the remote's IP address and the web configurator PIN.
|
|
18
18
|
|
|
19
|
-
## Quick start
|
|
19
|
+
## Quick start (for humans)
|
|
20
20
|
|
|
21
|
-
Before you start
|
|
22
|
-
|
|
21
|
+
### 1. Before you start
|
|
22
|
+
|
|
23
|
+
- Install [Node.js](https://nodejs.org/) 22 or newer.
|
|
24
|
+
- On the remote, enable the web configurator: **Settings → Profile → Web
|
|
25
|
+
configurator**. Note the **PIN** it shows.
|
|
26
|
+
- Find the remote's **IP address**. It's shown in the remote's network
|
|
27
|
+
settings, or in your router's device list. The computer you use must be on the same network as the remote.
|
|
28
|
+
|
|
29
|
+
### 2. Create your config folder
|
|
23
30
|
|
|
24
31
|
```sh
|
|
25
32
|
mkdir my-remote && cd my-remote
|
|
26
|
-
npx uc-config init
|
|
33
|
+
npx uc-config init
|
|
27
34
|
npm install
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
`init` creates the project files, then asks for the remote's IP address and
|
|
38
|
+
the web configurator PIN. The PIN is typed hidden in your terminal and is only
|
|
39
|
+
used once, to create an API key saved in `.uc/credentials.json`. Press Enter at
|
|
40
|
+
either prompt to skip it; you can rerun `npx uc-config init` at any time and it
|
|
41
|
+
picks up where it left off without overwriting anything.
|
|
42
|
+
|
|
43
|
+
If `init` says a key named `uc-config` already exists, revoke that key in the
|
|
44
|
+
web configurator and run `npx uc-config init` again.
|
|
45
|
+
|
|
46
|
+
This folder holds your remote's configuration. It's yours, not part of this
|
|
47
|
+
repo.
|
|
48
|
+
|
|
49
|
+
### 3. Let your coding agent set it up
|
|
50
|
+
|
|
51
|
+
Start your coding agent (Claude Code, Codex, Cursor, etc.) in that folder and
|
|
52
|
+
prompt:
|
|
31
53
|
|
|
32
54
|
> Set up my Remote 3 at `<IP>`
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
The agent imports your current setup into `remote.config.ts` and records which
|
|
57
|
+
resources it manages. This writes nothing to the remote.
|
|
58
|
+
|
|
59
|
+
If you skipped the PIN during `init`, the agent will ask you to run
|
|
60
|
+
`npx uc-config auth` in your own terminal. **Don't paste the PIN into the
|
|
61
|
+
chat.**
|
|
62
|
+
|
|
63
|
+
`init` also writes `CLAUDE.md`, which points Claude Code at `AGENTS.md`;
|
|
64
|
+
Claude Code reads `CLAUDE.md` rather than `AGENTS.md`.
|
|
65
|
+
|
|
66
|
+
### 4. Save your config
|
|
67
|
+
|
|
68
|
+
Commit the folder to a **private** git repo. It contains your device IDs and
|
|
69
|
+
IP addresses. `.uc/` (credentials and state) is already gitignored; back it up
|
|
70
|
+
separately.
|
|
71
|
+
|
|
72
|
+
### Day to day
|
|
73
|
+
|
|
74
|
+
Ask your agent for changes in plain language, for example:
|
|
75
|
+
|
|
76
|
+
> Make the NEXT button skip chapters in the movie activity
|
|
77
|
+
|
|
78
|
+
> Add a page to Watch TV with buttons for Netflix and YouTube
|
|
79
|
+
|
|
80
|
+
> Something on the remote says "orphaned entity". Fix it.
|
|
81
|
+
|
|
82
|
+
The agent shows a plan of what will change before applying it. After you update
|
|
83
|
+
an integration in the Integration Manager, ask the agent to run diagnostics.
|
|
37
84
|
|
|
38
85
|
## Requirements
|
|
39
86
|
|
|
@@ -57,6 +104,9 @@ npm install
|
|
|
57
104
|
npm run build
|
|
58
105
|
npm test # offline, uses a mock Core API
|
|
59
106
|
|
|
107
|
+
# Steps 2-3 are usually already done by `npx uc-config init`. Skip them if
|
|
108
|
+
# .uc/targets/*.json and .uc/credentials.json exist.
|
|
109
|
+
|
|
60
110
|
# 2. Register the remote under a target name ("home" here). With a single
|
|
61
111
|
# target, later commands pick it automatically; otherwise pass --target.
|
|
62
112
|
npm run uc -- connect home --host http://<REMOTE_IP>
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeFile, mkdir } from "node:fs/promises";
|
|
|
4
4
|
import { resolve, join } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { Writable } from "node:stream";
|
|
7
|
-
import { CoreClient, resolveSecrets } from "./client.js";
|
|
7
|
+
import { ApiError, CoreClient, resolveSecrets } from "./client.js";
|
|
8
8
|
import { Adapter } from "./adapter.js";
|
|
9
9
|
import { compile, validateConfig } from "./compiler.js";
|
|
10
10
|
import { Engine, PendingSetup } from "./engine.js";
|
|
@@ -123,7 +123,18 @@ program
|
|
|
123
123
|
.requiredOption("--host <url>")
|
|
124
124
|
.option("--token-env <name>", "API key environment variable", "UC_API_KEY")
|
|
125
125
|
.action(async (name, o) => {
|
|
126
|
-
|
|
126
|
+
await connectTarget(name, o.host, o.tokenEnv);
|
|
127
|
+
});
|
|
128
|
+
program
|
|
129
|
+
.command("auth")
|
|
130
|
+
.option("--target <name>", "target", defaultTarget)
|
|
131
|
+
.action(async (o) => {
|
|
132
|
+
await load(o.target); // fail on a missing target before prompting
|
|
133
|
+
const pin = process.env.UC_PIN ?? (await hiddenPrompt("Web configurator PIN: "));
|
|
134
|
+
await authenticate(o.target, pin);
|
|
135
|
+
});
|
|
136
|
+
async function connectTarget(name, host, tokenEnv = "UC_API_KEY") {
|
|
137
|
+
const client = new CoreClient(host);
|
|
127
138
|
const version = await client.version();
|
|
128
139
|
if (version.model !== "UCR3" || typeof version.address !== "string")
|
|
129
140
|
throw new Error("Expected an identifiable Remote 3");
|
|
@@ -131,7 +142,7 @@ program
|
|
|
131
142
|
host: client.base.origin,
|
|
132
143
|
identity: version.address,
|
|
133
144
|
version,
|
|
134
|
-
tokenEnv
|
|
145
|
+
tokenEnv,
|
|
135
146
|
};
|
|
136
147
|
try {
|
|
137
148
|
const old = await readJson(targetFile(name));
|
|
@@ -144,35 +155,54 @@ program
|
|
|
144
155
|
}
|
|
145
156
|
await saveJson(targetFile(name), target);
|
|
146
157
|
console.log(`Connected ${name}: ${version.model}, core ${version.core}, API ${version.api}. No configuration changed.`);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
const { target } = await load(o.target);
|
|
153
|
-
const pin = process.env.UC_PIN ?? (await hiddenPrompt("Web configurator PIN: "));
|
|
154
|
-
const client = new CoreClient(target.host);
|
|
155
|
-
validateRequest("/auth/api_keys", "POST", {
|
|
156
|
-
name: "uc-config",
|
|
157
|
-
scopes: ["admin"],
|
|
158
|
-
});
|
|
159
|
-
const { data } = await client.request("POST", "/auth/api_keys", { name: "uc-config", scopes: ["admin"] }, {
|
|
160
|
-
Authorization: `Basic ${Buffer.from(`web-configurator:${pin}`).toString("base64")}`,
|
|
161
|
-
});
|
|
162
|
-
if (typeof data.api_key !== "string")
|
|
163
|
-
throw new Error("No API key returned");
|
|
164
|
-
let credentials = {};
|
|
158
|
+
return target;
|
|
159
|
+
}
|
|
160
|
+
async function hasCredentials(target) {
|
|
161
|
+
if (process.env[target.tokenEnv])
|
|
162
|
+
return true;
|
|
165
163
|
try {
|
|
166
|
-
|
|
164
|
+
const c = await readJson(local("credentials.json"));
|
|
165
|
+
return typeof c[target.identity] === "string";
|
|
167
166
|
}
|
|
168
|
-
catch
|
|
169
|
-
|
|
170
|
-
throw e;
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
171
169
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
170
|
+
}
|
|
171
|
+
async function authenticate(name, pin) {
|
|
172
|
+
{
|
|
173
|
+
const { target } = await load(name);
|
|
174
|
+
const client = new CoreClient(target.host);
|
|
175
|
+
validateRequest("/auth/api_keys", "POST", {
|
|
176
|
+
name: "uc-config",
|
|
177
|
+
scopes: ["admin"],
|
|
178
|
+
});
|
|
179
|
+
const { data } = await client.request("POST", "/auth/api_keys", { name: "uc-config", scopes: ["admin"] }, {
|
|
180
|
+
Authorization: `Basic ${Buffer.from(`web-configurator:${pin}`).toString("base64")}`,
|
|
181
|
+
});
|
|
182
|
+
if (typeof data.api_key !== "string")
|
|
183
|
+
throw new Error("No API key returned");
|
|
184
|
+
let credentials = {};
|
|
185
|
+
try {
|
|
186
|
+
credentials = await readJson(local("credentials.json"));
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (e.code !== "ENOENT")
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
credentials[target.identity] = data.api_key;
|
|
193
|
+
await saveJson(local("credentials.json"), credentials);
|
|
194
|
+
console.log("API key saved in .uc/credentials.json (mode 0600, gitignored). Approve it on the remote if requested, then run doctor.");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function question(label) {
|
|
198
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
199
|
+
try {
|
|
200
|
+
return (await rl.question(label)).trim();
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
rl.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
176
206
|
program
|
|
177
207
|
.command("doctor")
|
|
178
208
|
.option("--target <name>", "target", defaultTarget)
|
|
@@ -506,7 +536,10 @@ ir.command("capture <emitterId>")
|
|
|
506
536
|
program
|
|
507
537
|
.command("init")
|
|
508
538
|
.description("Scaffold a private config workspace (package.json, tsconfig, .gitignore, AGENTS.md)")
|
|
509
|
-
.
|
|
539
|
+
.option("--host <ip>", "remote IP address (skips the prompt)")
|
|
540
|
+
.option("--target <name>", "target name", "home")
|
|
541
|
+
.option("--no-connect", "only create files; don't connect or authenticate")
|
|
542
|
+
.action(async (o) => {
|
|
510
543
|
const r = await init(root(), pkgVersion);
|
|
511
544
|
for (const f of r.written)
|
|
512
545
|
console.log(`created ${f}`);
|
|
@@ -514,7 +547,69 @@ program
|
|
|
514
547
|
console.log("added uc-config to package.json");
|
|
515
548
|
for (const f of r.skipped)
|
|
516
549
|
console.log(`kept existing ${f}`);
|
|
517
|
-
|
|
550
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
551
|
+
const next = "Next: npm install, then start your coding agent here and ask it to set up your Remote 3.";
|
|
552
|
+
if (!o.connect)
|
|
553
|
+
return console.log(next);
|
|
554
|
+
const name = o.target;
|
|
555
|
+
// 1. Connect (unless already connected).
|
|
556
|
+
let target;
|
|
557
|
+
try {
|
|
558
|
+
target = await readJson(targetFile(name));
|
|
559
|
+
console.log(`Already connected to ${target.host} as "${name}".`);
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
if (e.code !== "ENOENT")
|
|
563
|
+
throw e;
|
|
564
|
+
}
|
|
565
|
+
if (!target) {
|
|
566
|
+
const host = o.host ??
|
|
567
|
+
(interactive
|
|
568
|
+
? await question("Remote 3 IP address (blank to skip): ")
|
|
569
|
+
: "");
|
|
570
|
+
if (!host) {
|
|
571
|
+
console.log(`Skipped connecting. Later: npx uc-config connect ${name} --host http://<IP>\n${next}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
target = await connectTarget(name, host);
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
console.log(`Could not reach a Remote 3 at ${host}: ${e.message}\n` +
|
|
579
|
+
"Check the IP, that this computer is on the same network, and that the remote is awake (pick it up).\n" +
|
|
580
|
+
`Retry: npx uc-config init --host <IP> (files already created are kept)`);
|
|
581
|
+
process.exitCode = 1;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// 2. Authenticate (unless a key is already available).
|
|
586
|
+
if (await hasCredentials(target)) {
|
|
587
|
+
console.log("Already authenticated.");
|
|
588
|
+
return console.log(next);
|
|
589
|
+
}
|
|
590
|
+
const pin = process.env.UC_PIN ??
|
|
591
|
+
(interactive
|
|
592
|
+
? await hiddenPrompt("Web configurator PIN (Settings → Profile → Web configurator; blank to skip): ")
|
|
593
|
+
: "");
|
|
594
|
+
if (!pin) {
|
|
595
|
+
console.log("Skipped authentication. Run `npx uc-config auth` in this folder when ready.\n" +
|
|
596
|
+
next);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
await authenticate(name, pin);
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
const status = e instanceof ApiError ? e.status : 0;
|
|
604
|
+
console.log(status === 401 || status === 403
|
|
605
|
+
? "The remote rejected the PIN. Check that the web configurator is enabled and the PIN is current, then run `npx uc-config auth`."
|
|
606
|
+
: status === 400 || status === 409 || status === 422
|
|
607
|
+
? 'An API key named "uc-config" already exists on the remote. Revoke it in the web configurator, then run `npx uc-config auth`.'
|
|
608
|
+
: `Authentication failed: ${e.message}. Retry with \`npx uc-config auth\`.`);
|
|
609
|
+
process.exitCode = 1;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
console.log(next);
|
|
518
613
|
});
|
|
519
614
|
program
|
|
520
615
|
.command("diagnose")
|
package/dist/init.js
CHANGED
|
@@ -10,11 +10,17 @@ managed with the \`uc-config\` CLI (v${version}). Reference docs ship with the p
|
|
|
10
10
|
- node_modules/uc-config/docs/cli.md: every command, recovery
|
|
11
11
|
- node_modules/uc-config/docs/configuration-authoring.md: config syntax and ownership
|
|
12
12
|
|
|
13
|
-
## First run
|
|
13
|
+
## First run
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
\`npx uc-config init\` usually already connected and authenticated. Check what
|
|
16
|
+
exists, and skip steps that are done:
|
|
17
|
+
|
|
18
|
+
1. No \`.uc/targets/*.json\`: run \`npx uc-config connect home --host http://<IP>\`.
|
|
19
|
+
Ask the user for the IP if not given.
|
|
20
|
+
2. No \`.uc/credentials.json\` (and no \`UC_API_KEY\` env): ask the user to run
|
|
21
|
+
\`npx uc-config auth\` in their own terminal (the web configurator must be
|
|
22
|
+
enabled on the remote). Never ask for the PIN in chat, and never run \`auth\`
|
|
23
|
+
yourself; it needs a real terminal.
|
|
18
24
|
3. \`npx uc-config doctor\`
|
|
19
25
|
4. \`npx uc-config inventory --bindings generated/devices.ts\`
|
|
20
26
|
5. \`npx uc-config import --out remote.config.ts\`
|
|
@@ -52,6 +58,15 @@ the UC Integration Manager (http://<remote>:9999); run diagnose after any update
|
|
|
52
58
|
\`npm update uc-config\`, then compile and plan. The plan must show 0 operations
|
|
53
59
|
before you make any other change.
|
|
54
60
|
`;
|
|
61
|
+
// Claude Code reads CLAUDE.md, not AGENTS.md. Newer releases follow the
|
|
62
|
+
// @-import; the prose line covers releases that don't support imports.
|
|
63
|
+
const claude = `# Remote 3 configuration (uc-config)
|
|
64
|
+
|
|
65
|
+
Read AGENTS.md in this folder before doing anything. It is the authoritative
|
|
66
|
+
guide for working here: setup steps, editing rules and safety limits.
|
|
67
|
+
|
|
68
|
+
@AGENTS.md
|
|
69
|
+
`;
|
|
55
70
|
const tsconfig = {
|
|
56
71
|
compilerOptions: {
|
|
57
72
|
target: "ES2023",
|
|
@@ -120,5 +135,6 @@ export async function init(dir, version) {
|
|
|
120
135
|
await put("tsconfig.json", JSON.stringify(tsconfig, null, 2) + "\n");
|
|
121
136
|
await put(".gitignore", gitignore);
|
|
122
137
|
await put("AGENTS.md", agents(version));
|
|
138
|
+
await put("CLAUDE.md", claude);
|
|
123
139
|
return result;
|
|
124
140
|
}
|
package/docs/cli.md
CHANGED
|
@@ -9,33 +9,37 @@ the workspace's only target if exactly one is connected; otherwise `home`.
|
|
|
9
9
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
12
|
-
| Command | Writes to remote | Purpose
|
|
13
|
-
| ---------------------------------------------- | ----------------- |
|
|
14
|
-
| `init`
|
|
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.
|
|
12
|
+
| Command | Writes to remote | Purpose |
|
|
13
|
+
| ---------------------------------------------- | ----------------- | ------------------------------------------------------------------------------ |
|
|
14
|
+
| `init [--host ip] [--no-connect]` | API key only | Scaffold a workspace, then prompt for IP and PIN (connect + auth). Rerunnable. |
|
|
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
33
|
|
|
34
34
|
Exit codes: `0` ok, `1` error, `2` drift/conflicts/deferred/diagnose findings,
|
|
35
35
|
`3` paused for a human step.
|
|
36
36
|
|
|
37
37
|
## Authentication
|
|
38
38
|
|
|
39
|
+
`init` runs `connect` and `auth` for you when started in a terminal. Without a
|
|
40
|
+
terminal (e.g. run by an agent) it only scaffolds files unless `--host` and
|
|
41
|
+
`UC_PIN` are supplied. Steps already completed are skipped on reruns.
|
|
42
|
+
|
|
39
43
|
`auth` prompts for the web configurator PIN. Enable the web configurator on the
|
|
40
44
|
remote first, and approve the key on the remote if asked. Non-interactive
|
|
41
45
|
options: set `UC_PIN`, or set `UC_API_KEY` to an existing key (`connect
|