uc-config 0.1.0 → 0.2.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 +35 -0
- package/README.md +75 -8
- package/dist/cli.js +165 -32
- package/dist/init.d.ts +9 -1
- package/dist/init.js +71 -8
- package/docs/cli.md +26 -21
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Each release says whether upgrading needs any **action**. To upgrade a config
|
|
4
|
+
workspace:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install uc-config@latest
|
|
8
|
+
npx uc-config init --refresh-docs
|
|
9
|
+
npx uc-config compile && npx uc-config plan # must show 0 operations
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 0.2.1
|
|
13
|
+
|
|
14
|
+
Action required: run `npx uc-config init --refresh-docs`. It updates the
|
|
15
|
+
scaffolded AGENTS.md, whose upgrade instructions were wrong.
|
|
16
|
+
|
|
17
|
+
- `init --refresh-docs` updates AGENTS.md and CLAUDE.md to the installed
|
|
18
|
+
version. Files you edited are kept, and the new version is written beside
|
|
19
|
+
them as `.new`.
|
|
20
|
+
- `doctor` prints a notice when a newer version is published. Set
|
|
21
|
+
`UC_NO_UPDATE_CHECK=1` to disable it.
|
|
22
|
+
- The scaffolded upgrade instructions use `npm install uc-config@latest`.
|
|
23
|
+
`npm update` doesn't move between 0.x minor versions.
|
|
24
|
+
|
|
25
|
+
## 0.2.0
|
|
26
|
+
|
|
27
|
+
Action required: none.
|
|
28
|
+
|
|
29
|
+
- `init` asks for the remote's IP and the web configurator PIN, then connects
|
|
30
|
+
and authenticates. It can be rerun safely.
|
|
31
|
+
- `init` writes CLAUDE.md, which points Claude Code at AGENTS.md.
|
|
32
|
+
|
|
33
|
+
## 0.1.0
|
|
34
|
+
|
|
35
|
+
First release.
|
package/README.md
CHANGED
|
@@ -16,24 +16,88 @@ 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.
|
|
84
|
+
|
|
85
|
+
## Updating
|
|
86
|
+
|
|
87
|
+
Ask your agent to "update uc-config", or run in your config folder:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npm install uc-config@latest
|
|
91
|
+
npx uc-config init --refresh-docs # refresh AGENTS.md / CLAUDE.md
|
|
92
|
+
npx uc-config compile && npx uc-config plan # must show 0 operations
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use `npm install uc-config@latest` rather than `npm update`: before 1.0,
|
|
96
|
+
`npm update` doesn't move between minor versions (0.2 → 0.3). `--refresh-docs`
|
|
97
|
+
keeps files you've edited and writes the new version beside them as `.new`.
|
|
98
|
+
`doctor` tells you when a newer version is available. See
|
|
99
|
+
[CHANGELOG.md](CHANGELOG.md) for what changed and whether a release needs any
|
|
100
|
+
action.
|
|
37
101
|
|
|
38
102
|
## Requirements
|
|
39
103
|
|
|
@@ -57,6 +121,9 @@ npm install
|
|
|
57
121
|
npm run build
|
|
58
122
|
npm test # offline, uses a mock Core API
|
|
59
123
|
|
|
124
|
+
# Steps 2-3 are usually already done by `npx uc-config init`. Skip them if
|
|
125
|
+
# .uc/targets/*.json and .uc/credentials.json exist.
|
|
126
|
+
|
|
60
127
|
# 2. Register the remote under a target name ("home" here). With a single
|
|
61
128
|
# target, later commands pick it automatically; otherwise pass --target.
|
|
62
129
|
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,79 @@ 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
|
+
/** Latest published version, or undefined if offline/disabled. Never throws. */
|
|
198
|
+
async function latestVersion() {
|
|
199
|
+
if (process.env.UC_NO_UPDATE_CHECK)
|
|
200
|
+
return undefined;
|
|
201
|
+
try {
|
|
202
|
+
const res = await fetch("https://registry.npmjs.org/uc-config/latest", {
|
|
203
|
+
signal: AbortSignal.timeout(3000),
|
|
204
|
+
});
|
|
205
|
+
if (!res.ok)
|
|
206
|
+
return undefined;
|
|
207
|
+
const v = (await res.json()).version;
|
|
208
|
+
return typeof v === "string" ? v : undefined;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function newer(a, b) {
|
|
215
|
+
const p = (v) => v.split("-")[0].split(".").map(Number);
|
|
216
|
+
const [x, y] = [p(a), p(b)];
|
|
217
|
+
for (let i = 0; i < 3; i++)
|
|
218
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
219
|
+
return (x[i] ?? 0) > (y[i] ?? 0);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
async function question(label) {
|
|
223
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
224
|
+
try {
|
|
225
|
+
return (await rl.question(label)).trim();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
rl.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
176
231
|
program
|
|
177
232
|
.command("doctor")
|
|
178
233
|
.option("--target <name>", "target", defaultTarget)
|
|
@@ -180,6 +235,9 @@ program
|
|
|
180
235
|
const { client, target } = await load(o.target);
|
|
181
236
|
await client.verifyTarget(target);
|
|
182
237
|
console.log(`Identity verified; core ${target.version.core}, reported API ${target.version.api}`);
|
|
238
|
+
const latest = await latestVersion();
|
|
239
|
+
if (latest && newer(latest, pkgVersion))
|
|
240
|
+
console.log(`Update available: uc-config ${pkgVersion} -> ${latest}. Run: npm install uc-config@latest && npx uc-config init --refresh-docs (see CHANGELOG.md)`);
|
|
183
241
|
for (const path of [
|
|
184
242
|
"/entities",
|
|
185
243
|
"/activities",
|
|
@@ -506,15 +564,90 @@ ir.command("capture <emitterId>")
|
|
|
506
564
|
program
|
|
507
565
|
.command("init")
|
|
508
566
|
.description("Scaffold a private config workspace (package.json, tsconfig, .gitignore, AGENTS.md)")
|
|
509
|
-
.
|
|
510
|
-
|
|
567
|
+
.option("--host <ip>", "remote IP address (skips the prompt)")
|
|
568
|
+
.option("--target <name>", "target name", "home")
|
|
569
|
+
.option("--no-connect", "only create files; don't connect or authenticate")
|
|
570
|
+
.option("--refresh-docs", "update AGENTS.md/CLAUDE.md to this version (edited files get a .new copy)")
|
|
571
|
+
.action(async (o) => {
|
|
572
|
+
const r = await init(root(), pkgVersion, { refreshDocs: o.refreshDocs });
|
|
511
573
|
for (const f of r.written)
|
|
512
574
|
console.log(`created ${f}`);
|
|
513
575
|
if (r.packageJsonUpdated)
|
|
514
576
|
console.log("added uc-config to package.json");
|
|
577
|
+
for (const f of r.refreshed)
|
|
578
|
+
console.log(`updated ${f} to v${pkgVersion}`);
|
|
579
|
+
for (const f of r.conflicts)
|
|
580
|
+
console.log(`kept ${f} (edited); wrote ${f}.new. Merge your changes, then delete ${f}.new`);
|
|
515
581
|
for (const f of r.skipped)
|
|
516
582
|
console.log(`kept existing ${f}`);
|
|
517
|
-
|
|
583
|
+
if (o.refreshDocs) {
|
|
584
|
+
if (r.conflicts.length)
|
|
585
|
+
process.exitCode = 2;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
589
|
+
const next = "Next: npm install, then start your coding agent here and ask it to set up your Remote 3.";
|
|
590
|
+
if (!o.connect)
|
|
591
|
+
return console.log(next);
|
|
592
|
+
const name = o.target;
|
|
593
|
+
// 1. Connect (unless already connected).
|
|
594
|
+
let target;
|
|
595
|
+
try {
|
|
596
|
+
target = await readJson(targetFile(name));
|
|
597
|
+
console.log(`Already connected to ${target.host} as "${name}".`);
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
if (e.code !== "ENOENT")
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
if (!target) {
|
|
604
|
+
const host = o.host ??
|
|
605
|
+
(interactive
|
|
606
|
+
? await question("Remote 3 IP address (blank to skip): ")
|
|
607
|
+
: "");
|
|
608
|
+
if (!host) {
|
|
609
|
+
console.log(`Skipped connecting. Later: npx uc-config connect ${name} --host http://<IP>\n${next}`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
target = await connectTarget(name, host);
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
console.log(`Could not reach a Remote 3 at ${host}: ${e.message}\n` +
|
|
617
|
+
"Check the IP, that this computer is on the same network, and that the remote is awake (pick it up).\n" +
|
|
618
|
+
`Retry: npx uc-config init --host <IP> (files already created are kept)`);
|
|
619
|
+
process.exitCode = 1;
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// 2. Authenticate (unless a key is already available).
|
|
624
|
+
if (await hasCredentials(target)) {
|
|
625
|
+
console.log("Already authenticated.");
|
|
626
|
+
return console.log(next);
|
|
627
|
+
}
|
|
628
|
+
const pin = process.env.UC_PIN ??
|
|
629
|
+
(interactive
|
|
630
|
+
? await hiddenPrompt("Web configurator PIN (Settings → Profile → Web configurator; blank to skip): ")
|
|
631
|
+
: "");
|
|
632
|
+
if (!pin) {
|
|
633
|
+
console.log("Skipped authentication. Run `npx uc-config auth` in this folder when ready.\n" +
|
|
634
|
+
next);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
await authenticate(name, pin);
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
const status = e instanceof ApiError ? e.status : 0;
|
|
642
|
+
console.log(status === 401 || status === 403
|
|
643
|
+
? "The remote rejected the PIN. Check that the web configurator is enabled and the PIN is current, then run `npx uc-config auth`."
|
|
644
|
+
: status === 400 || status === 409 || status === 422
|
|
645
|
+
? 'An API key named "uc-config" already exists on the remote. Revoke it in the web configurator, then run `npx uc-config auth`.'
|
|
646
|
+
: `Authentication failed: ${e.message}. Retry with \`npx uc-config auth\`.`);
|
|
647
|
+
process.exitCode = 1;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
console.log(next);
|
|
518
651
|
});
|
|
519
652
|
program
|
|
520
653
|
.command("diagnose")
|
package/dist/init.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
/** True when the file is unedited generated output from any uc-config version. */
|
|
2
|
+
export declare function isPristine(content: string): boolean;
|
|
1
3
|
export interface InitResult {
|
|
2
4
|
written: string[];
|
|
3
5
|
skipped: string[];
|
|
4
6
|
packageJsonUpdated: boolean;
|
|
7
|
+
/** Generated docs replaced by --refresh-docs. */
|
|
8
|
+
refreshed: string[];
|
|
9
|
+
/** Edited docs kept; the new version was written to `<name>.new`. */
|
|
10
|
+
conflicts: string[];
|
|
5
11
|
}
|
|
6
12
|
/** Scaffold a private config workspace. Never overwrites existing files. */
|
|
7
|
-
export declare function init(dir: string, version: string
|
|
13
|
+
export declare function init(dir: string, version: string, options?: {
|
|
14
|
+
refreshDocs?: boolean;
|
|
15
|
+
}): Promise<InitResult>;
|
package/dist/init.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
5
|
+
const MARKER = /^<!-- uc-config:generated v(\S+) sha256:([0-9a-f]{64}) -->\n/;
|
|
6
|
+
/** Prefix generated docs with a marker so --refresh-docs can detect edits. */
|
|
7
|
+
const stamp = (version, body) => `<!-- uc-config:generated v${version} sha256:${sha256(body)} -->\n${body}`;
|
|
8
|
+
/** Exact output of releases that predate the marker (0.1.0, 0.2.0). */
|
|
9
|
+
const LEGACY = new Set([
|
|
10
|
+
"053dd83c530adb91f58ad52148c9d858cc28864a137b7b5e65c4563338e91b4f",
|
|
11
|
+
"159e847a6a5b6c5cb9ed96f400e8f5d58cd9acae2e68bf666fc3fae2e4ad249b",
|
|
12
|
+
"d71e02c349b769c916d166a6b26cc14d80da4142042fa90564bda85c897a9b07",
|
|
13
|
+
]);
|
|
14
|
+
/** True when the file is unedited generated output from any uc-config version. */
|
|
15
|
+
export function isPristine(content) {
|
|
16
|
+
const m = MARKER.exec(content);
|
|
17
|
+
if (m)
|
|
18
|
+
return sha256(content.slice(m[0].length)) === m[2];
|
|
19
|
+
return LEGACY.has(sha256(content));
|
|
20
|
+
}
|
|
3
21
|
const agents = (version) => `# Remote 3 configuration (uc-config)
|
|
4
22
|
|
|
5
23
|
This folder holds the user's Unfolded Circle Remote 3 configuration. It is
|
|
@@ -10,11 +28,17 @@ managed with the \`uc-config\` CLI (v${version}). Reference docs ship with the p
|
|
|
10
28
|
- node_modules/uc-config/docs/cli.md: every command, recovery
|
|
11
29
|
- node_modules/uc-config/docs/configuration-authoring.md: config syntax and ownership
|
|
12
30
|
|
|
13
|
-
## First run
|
|
31
|
+
## First run
|
|
32
|
+
|
|
33
|
+
\`npx uc-config init\` usually already connected and authenticated. Check what
|
|
34
|
+
exists, and skip steps that are done:
|
|
14
35
|
|
|
15
|
-
1. \`npx uc-config connect home --host http://<IP>\`.
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
1. No \`.uc/targets/*.json\`: run \`npx uc-config connect home --host http://<IP>\`.
|
|
37
|
+
Ask the user for the IP if not given.
|
|
38
|
+
2. No \`.uc/credentials.json\` (and no \`UC_API_KEY\` env): ask the user to run
|
|
39
|
+
\`npx uc-config auth\` in their own terminal (the web configurator must be
|
|
40
|
+
enabled on the remote). Never ask for the PIN in chat, and never run \`auth\`
|
|
41
|
+
yourself; it needs a real terminal.
|
|
18
42
|
3. \`npx uc-config doctor\`
|
|
19
43
|
4. \`npx uc-config inventory --bindings generated/devices.ts\`
|
|
20
44
|
5. \`npx uc-config import --out remote.config.ts\`
|
|
@@ -49,8 +73,22 @@ the UC Integration Manager (http://<remote>:9999); run diagnose after any update
|
|
|
49
73
|
|
|
50
74
|
## Upgrading the tool
|
|
51
75
|
|
|
52
|
-
\`npm
|
|
53
|
-
|
|
76
|
+
1. \`npm install uc-config@latest\` (not \`npm update\`: it won't cross 0.x minor versions).
|
|
77
|
+
2. \`npx uc-config init --refresh-docs\` to update this file and CLAUDE.md.
|
|
78
|
+
Files the user edited are kept; the new version is written beside them as \`.new\`.
|
|
79
|
+
3. Read node_modules/uc-config/CHANGELOG.md for any "Action required" notes.
|
|
80
|
+
4. \`npx uc-config compile && npx uc-config plan\`. It must show 0 operations
|
|
81
|
+
before you make any other change. If not, the upgrade changed how the config
|
|
82
|
+
is read: show the user the plan and don't apply it.
|
|
83
|
+
`;
|
|
84
|
+
// Claude Code reads CLAUDE.md, not AGENTS.md. Newer releases follow the
|
|
85
|
+
// @-import; the prose line covers releases that don't support imports.
|
|
86
|
+
const claude = `# Remote 3 configuration (uc-config)
|
|
87
|
+
|
|
88
|
+
Read AGENTS.md in this folder before doing anything. It is the authoritative
|
|
89
|
+
guide for working here: setup steps, editing rules and safety limits.
|
|
90
|
+
|
|
91
|
+
@AGENTS.md
|
|
54
92
|
`;
|
|
55
93
|
const tsconfig = {
|
|
56
94
|
compilerOptions: {
|
|
@@ -70,12 +108,36 @@ const gitignore = `node_modules/
|
|
|
70
108
|
.env.*
|
|
71
109
|
`;
|
|
72
110
|
/** Scaffold a private config workspace. Never overwrites existing files. */
|
|
73
|
-
export async function init(dir, version) {
|
|
111
|
+
export async function init(dir, version, options = {}) {
|
|
74
112
|
await mkdir(dir, { recursive: true });
|
|
75
113
|
const result = {
|
|
76
114
|
written: [],
|
|
77
115
|
skipped: [],
|
|
78
116
|
packageJsonUpdated: false,
|
|
117
|
+
refreshed: [],
|
|
118
|
+
conflicts: [],
|
|
119
|
+
};
|
|
120
|
+
const doc = async (name, contents) => {
|
|
121
|
+
let existing;
|
|
122
|
+
try {
|
|
123
|
+
existing = await readFile(join(dir, name), "utf8");
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
if (e.code !== "ENOENT")
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
if (existing === undefined)
|
|
130
|
+
return put(name, contents);
|
|
131
|
+
if (!options.refreshDocs || existing === contents)
|
|
132
|
+
return void result.skipped.push(name);
|
|
133
|
+
if (isPristine(existing)) {
|
|
134
|
+
await writeFile(join(dir, name), contents);
|
|
135
|
+
result.refreshed.push(name);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await writeFile(join(dir, `${name}.new`), contents);
|
|
139
|
+
result.conflicts.push(name);
|
|
140
|
+
}
|
|
79
141
|
};
|
|
80
142
|
const put = async (name, contents) => {
|
|
81
143
|
try {
|
|
@@ -119,6 +181,7 @@ export async function init(dir, version) {
|
|
|
119
181
|
}
|
|
120
182
|
await put("tsconfig.json", JSON.stringify(tsconfig, null, 2) + "\n");
|
|
121
183
|
await put(".gitignore", gitignore);
|
|
122
|
-
await
|
|
184
|
+
await doc("AGENTS.md", stamp(version, agents(version)));
|
|
185
|
+
await doc("CLAUDE.md", stamp(version, claude));
|
|
123
186
|
return result;
|
|
124
187
|
}
|
package/docs/cli.md
CHANGED
|
@@ -9,33 +9,38 @@ 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
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
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
|
+
| `init --refresh-docs` | no | Update AGENTS.md/CLAUDE.md; edited files are kept and get a `.new` copy. |
|
|
16
|
+
| `connect <name> --host <url>` | no | Record a target (identity, firmware). Rerun after firmware updates. |
|
|
17
|
+
| `auth` | API key only | Exchange the web-configurator PIN (`UC_PIN` or prompt) for an API key. |
|
|
18
|
+
| `doctor` | no | Verify identity and read access to every required endpoint. |
|
|
19
|
+
| `diagnose [--json]` | no | Orphaned entity references, disconnected integrations, suggested fixes. |
|
|
20
|
+
| `inventory [--out f] [--bindings f.ts]` | no | Raw (redacted) remote state; optional typed entity/command bindings. |
|
|
21
|
+
| `import [--out f.ts]` | no | Generate editable config from the live remote. Never overwrites. |
|
|
22
|
+
| `compile [--config f] [--out f]` | no | Evaluate TS config into `.uc/build.json`. Offline. |
|
|
23
|
+
| `plan [--out f] [--prune] [--overwrite-drift]` | no | Three-way diff: source vs last-applied vs live. |
|
|
24
|
+
| `apply <plan> [--adopt-only]` | yes | Execute a saved plan after re-verifying preconditions. |
|
|
25
|
+
| `check` | no | Re-plan and exit 2 if anything differs. |
|
|
26
|
+
| `resume` | maybe | Continue paused setup; reconcile an interrupted apply. |
|
|
27
|
+
| `setup status/respond <key>` | yes | Drive interactive integration/dock setup. |
|
|
28
|
+
| `pairing status/respond <remoteId>` | yes | Bluetooth pairing steps. |
|
|
29
|
+
| `ir learn/capture <emitterId>` | yes | Learn IR codes from a physical remote. |
|
|
30
|
+
| `state adopt <key> <id>` / `forget` / `move` | no | Edit local ownership bindings. |
|
|
31
|
+
| `rollback [--out f]` | no | Build a compensating plan from the last journal. |
|
|
32
|
+
| `backup --out f` | stops intgs | Native full backup. Disruptive; not for routine use. |
|
|
33
|
+
| `api <METHOD> <path> [--data json] [--write]` | only with --write | Raw authenticated Core API call; output redacted. |
|
|
33
34
|
|
|
34
35
|
Exit codes: `0` ok, `1` error, `2` drift/conflicts/deferred/diagnose findings,
|
|
35
36
|
`3` paused for a human step.
|
|
36
37
|
|
|
37
38
|
## Authentication
|
|
38
39
|
|
|
40
|
+
`init` runs `connect` and `auth` for you when started in a terminal. Without a
|
|
41
|
+
terminal (e.g. run by an agent) it only scaffolds files unless `--host` and
|
|
42
|
+
`UC_PIN` are supplied. Steps already completed are skipped on reruns.
|
|
43
|
+
|
|
39
44
|
`auth` prompts for the web configurator PIN. Enable the web configurator on the
|
|
40
45
|
remote first, and approve the key on the remote if asked. Non-interactive
|
|
41
46
|
options: set `UC_PIN`, or set `UC_API_KEY` to an existing key (`connect
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uc-config",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"examples",
|
|
37
37
|
"remote.config.example.ts",
|
|
38
38
|
"README.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|