uc-config 0.2.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 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
@@ -82,6 +82,23 @@ Ask your agent for changes in plain language, for example:
82
82
  The agent shows a plan of what will change before applying it. After you update
83
83
  an integration in the Integration Manager, ask the agent to run diagnostics.
84
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.
101
+
85
102
  ## Requirements
86
103
 
87
104
  - Node.js 22+ and npm
package/dist/cli.js CHANGED
@@ -194,6 +194,31 @@ async function authenticate(name, pin) {
194
194
  console.log("API key saved in .uc/credentials.json (mode 0600, gitignored). Approve it on the remote if requested, then run doctor.");
195
195
  }
196
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
+ }
197
222
  async function question(label) {
198
223
  const rl = createInterface({ input: process.stdin, output: process.stdout });
199
224
  try {
@@ -210,6 +235,9 @@ program
210
235
  const { client, target } = await load(o.target);
211
236
  await client.verifyTarget(target);
212
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)`);
213
241
  for (const path of [
214
242
  "/entities",
215
243
  "/activities",
@@ -539,14 +567,24 @@ program
539
567
  .option("--host <ip>", "remote IP address (skips the prompt)")
540
568
  .option("--target <name>", "target name", "home")
541
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)")
542
571
  .action(async (o) => {
543
- const r = await init(root(), pkgVersion);
572
+ const r = await init(root(), pkgVersion, { refreshDocs: o.refreshDocs });
544
573
  for (const f of r.written)
545
574
  console.log(`created ${f}`);
546
575
  if (r.packageJsonUpdated)
547
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`);
548
581
  for (const f of r.skipped)
549
582
  console.log(`kept existing ${f}`);
583
+ if (o.refreshDocs) {
584
+ if (r.conflicts.length)
585
+ process.exitCode = 2;
586
+ return;
587
+ }
550
588
  const interactive = Boolean(process.stdin.isTTY);
551
589
  const next = "Next: npm install, then start your coding agent here and ask it to set up your Remote 3.";
552
590
  if (!o.connect)
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): Promise<InitResult>;
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
@@ -55,8 +73,13 @@ the UC Integration Manager (http://<remote>:9999); run diagnose after any update
55
73
 
56
74
  ## Upgrading the tool
57
75
 
58
- \`npm update uc-config\`, then compile and plan. The plan must show 0 operations
59
- before you make any other change.
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.
60
83
  `;
61
84
  // Claude Code reads CLAUDE.md, not AGENTS.md. Newer releases follow the
62
85
  // @-import; the prose line covers releases that don't support imports.
@@ -85,12 +108,36 @@ const gitignore = `node_modules/
85
108
  .env.*
86
109
  `;
87
110
  /** Scaffold a private config workspace. Never overwrites existing files. */
88
- export async function init(dir, version) {
111
+ export async function init(dir, version, options = {}) {
89
112
  await mkdir(dir, { recursive: true });
90
113
  const result = {
91
114
  written: [],
92
115
  skipped: [],
93
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
+ }
94
141
  };
95
142
  const put = async (name, contents) => {
96
143
  try {
@@ -134,7 +181,7 @@ export async function init(dir, version) {
134
181
  }
135
182
  await put("tsconfig.json", JSON.stringify(tsconfig, null, 2) + "\n");
136
183
  await put(".gitignore", gitignore);
137
- await put("AGENTS.md", agents(version));
138
- await put("CLAUDE.md", claude);
184
+ await doc("AGENTS.md", stamp(version, agents(version)));
185
+ await doc("CLAUDE.md", stamp(version, claude));
139
186
  return result;
140
187
  }
package/docs/cli.md CHANGED
@@ -12,6 +12,7 @@ the workspace's only target if exactly one is connected; otherwise `home`.
12
12
  | Command | Writes to remote | Purpose |
13
13
  | ---------------------------------------------- | ----------------- | ------------------------------------------------------------------------------ |
14
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. |
15
16
  | `connect <name> --host <url>` | no | Record a target (identity, firmware). Rerun after firmware updates. |
16
17
  | `auth` | API key only | Exchange the web-configurator PIN (`UC_PIN` or prompt) for an API key. |
17
18
  | `doctor` | no | Verify identity and read access to every required endpoint. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uc-config",
3
- "version": "0.2.0",
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": {