@abelxiaoxing/cadence 2.0.7 → 2.0.8

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 CHANGED
@@ -24,13 +24,14 @@ They may still report blocked scripts from the pinned Pi SDK's dependencies: `@g
24
24
  `cadence server install` registers and starts the background service for `~/cadence` on `http://127.0.0.1:17423`; `--workspace /absolute/project` selects another workspace.
25
25
  It prints the private link to open, `http://127.0.0.1:17423/?token=…`.
26
26
  The port and that token live with the research settings in `~/.pi/agent/cadence/.env`.
27
- Install creates that file from the research template when missing (your own 0600 file; Pi's startup card names the path until it exists), and adds either line when it is missing, never changing an existing one:
27
+ Install creates that file from the research template when missing (Pi's startup card names the path until it exists), and adds either line when it is missing, never changing an existing one:
28
28
 
29
29
  ```sh
30
30
  CADENCE_PORT=17423
31
31
  CADENCE_TOKEN=<random 43 characters>
32
32
  ```
33
33
 
34
+ A file you made yourself works as it is, like Pi's own sources: any permissions (a note suggests `chmod 600` when other users can read it) and dotfiles links; a refusal names the line and reason.
34
35
  The token admits anyone who has it to the workspace, so keep the file and the link private; a token you choose needs at least 32 characters.
35
36
  To change either, edit the line and run `cadence server restart`, which prints the new link; old links and browser cookies stop working.
36
37
  A workspace that an earlier version started with a generated token switches to the file at that restart and keeps its history.
@@ -375,6 +376,7 @@ Byte values are UTF-8/encoded bounds where specified; they are not targets to si
375
376
  | Pi's TUI replaces an info status with the next one | The missing-service hint is its own warning notification |
376
377
  | Token source the user edits (`CADENCE_TOKEN` in `.env`) | Read once before binding, never bound for the run: an edit cannot fence Stop |
377
378
  | Saved port/token source differs from `.env` | Only when stopped, reselect just those under the reservation; running ones refuse |
379
+ | User-made `.env` (0755/0644, umask 002, dotfiles links) | Like Pi's sources: ownership only, links followed; each refusal names its reason |
378
380
 
379
381
  Good: current context → explicit grant → durable acceptance → genuine original native/storage settlement → exact retained dependency → real checks → human review.
380
382
  Base: a client leaves and another reattaches to the same accepted run without launching it again.
@@ -418,6 +420,14 @@ Review the independently enumerated package member list and every emitted body,
418
420
  Keep public Pi peers external for the extension, the exact Undici/ws runtime edges, the exact optional SDK pins for the standalone owner and all upstream/font/vendor notices.
419
421
  Preserve original license bytes through their precise Git attributes; line-ending normalization must not invalidate audited source/notice hashes.
420
422
 
423
+ ### Release 2.0.8 acceptance snapshot
424
+
425
+ Full `verify` passed on the 2.0.8 release tree (`7a40be6` plus the version bump), in the 2.0.3 environment below.
426
+ It ran 1948 root tests in 78 files (27 Windows-only controls skipped on Linux), 111 web tests, 5 real-browser scenarios and the 132-member pack audit, and CI 40201203720 passed 12/12 on the candidate `1c372b2` (this commit before its snapshot).
427
+ The fix answers a 2.0.7 report: `cadence server install` refused existing `.env` files with one message.
428
+ The 2.0.7 refusal was reproduced for a 0755 directory from the older `mkdir -p` docs, 0644 editor files, umask 002 and a symlinked `~/.pi`.
429
+ Those layouts now install, a dotfiles link stays a link, and a remaining refusal (an `export` line) names its line.
430
+
421
431
  ### Release 2.0.7 acceptance snapshot
422
432
 
423
433
  Full `verify` ran on the 2.0.7 release tree (`7973e23` plus the version bump), in the 2.0.3 environment below.
@@ -68,9 +68,12 @@ CADENCE_TOKEN=<at least 32 characters, no control or format characters>
68
68
  ```
69
69
 
70
70
  The file keeps the research skills' syntax (`KEY=value` lines, `#` comments, one optional pair of quotes), and each of these keys may appear only once.
71
- `install` and `restart` create the file from the research template when it is missing, as your own 0600 file in a 0700 directory.
71
+ `install` and `restart` create the file from the research template when it is missing, as your own 0600 file.
72
72
  They append a missing `CADENCE_PORT` (17423) or `CADENCE_TOKEN` (random, 43 characters) under a comment, in the file's own line endings, and never change an existing line.
73
- An unusable file (another owner, wider permissions, a symlink, a syntax error the research skills would also refuse, a duplicate key, a port outside 1-65535, a short token) makes them change nothing, naming only its path.
73
+ It is your own file, made by hand, by older docs (`mkdir -p ~/.pi/agent/cadence`) or by a dotfiles manager, so it is read like Pi's sources: its mode bits are not checked (0644, or 0664 under umask 002, work), links in its path and the file itself are followed, and a rewrite goes to the real file, written 0600, leaving the links in place.
74
+ When other users can read it, `install` and `restart` add a note naming `chmod 600`, without refusing.
75
+ Only a file that cannot be the user's own configuration makes them change nothing: missing or a link to nothing, not a regular file, not owned by you or in a directory you do not own, hard-linked, over 64 KiB or not UTF-8, a line the research skills would also refuse, a duplicate key, a port outside 1-65535 or a short token.
76
+ Each refusal names its reason and line number, never a byte of the file.
74
77
  A project's `.pi/cadence/.env`, which the research skills prefer, is never read for the service.
75
78
  `start --server-config <.env>` makes these lines authoritative.
76
79
  The host reads them once before binding, like an environment token, so editing them while the owner runs changes nothing and can never fence access or the orderly Stop; the next start applies them, and `cadence server restart` does both.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Shared Pi task workspace: native terminal and WebUI, persistent service, checks and human review.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2511,13 +2511,13 @@ var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
2511
2511
  // src/service/autostart.ts
2512
2512
  import { spawnSync as spawnSync2 } from "node:child_process";
2513
2513
  import {
2514
- lstatSync as lstatSync3,
2514
+ lstatSync as lstatSync4,
2515
2515
  mkdirSync as mkdirSync4,
2516
2516
  readFileSync,
2517
- realpathSync as realpathSync2,
2517
+ realpathSync as realpathSync3,
2518
2518
  renameSync as renameSync3,
2519
2519
  rmSync,
2520
- statSync,
2520
+ statSync as statSync2,
2521
2521
  writeFileSync as writeFileSync3
2522
2522
  } from "node:fs";
2523
2523
  import { homedir as homedir3 } from "node:os";
@@ -2533,6 +2533,7 @@ import {
2533
2533
 
2534
2534
  // src/service/server-config.ts
2535
2535
  import { randomBytes } from "node:crypto";
2536
+ import { lstatSync as lstatSync3, realpathSync as realpathSync2, statSync } from "node:fs";
2536
2537
  import { dirname as dirname3, join as join3 } from "node:path";
2537
2538
 
2538
2539
  // skills/_shared/load-config.mjs
@@ -2832,8 +2833,39 @@ function accessToken(value) {
2832
2833
  function serverConfigPath(home) {
2833
2834
  return join3(home, ".pi", "agent", "cadence", ".env");
2834
2835
  }
2836
+
2837
+ class ServerConfigRefusal extends ContractError {
2838
+ path;
2839
+ reason;
2840
+ constructor(path2, reason) {
2841
+ super("corrupt-selected");
2842
+ this.path = path2;
2843
+ this.reason = reason;
2844
+ }
2845
+ }
2846
+
2847
+ class Unusable extends Error {
2848
+ }
2849
+ var unusable = (reason) => {
2850
+ throw new Unusable(reason);
2851
+ };
2852
+ function syntax(error) {
2853
+ const message = error instanceof Error ? error.message : "";
2854
+ const line = /at line (\d+)$/.exec(message)?.[1];
2855
+ if (!line)
2856
+ return "is not in KEY=value lines";
2857
+ if (message.startsWith("Invalid configuration name"))
2858
+ return `has an invalid name on line ${line} (letters, digits and _ only, no "export")`;
2859
+ if (message.startsWith("Unterminated"))
2860
+ return `has an unterminated quote on line ${line}`;
2861
+ return `has no KEY=value setting on line ${line}`;
2862
+ }
2835
2863
  function settings(source) {
2836
- parseEnvFile(source);
2864
+ try {
2865
+ parseEnvFile(source);
2866
+ } catch (error) {
2867
+ unusable(`${syntax(error)}, which the research skills refuse too`);
2868
+ }
2837
2869
  const parts = source.split(/(\r\n|\r|\n)/);
2838
2870
  const found = {};
2839
2871
  for (let at = 0;at < parts.length; at += 2) {
@@ -2845,83 +2877,137 @@ function settings(source) {
2845
2877
  const name = key === PORT_KEY ? "port" : key === TOKEN_KEY ? "token" : undefined;
2846
2878
  if (!name)
2847
2879
  continue;
2848
- if (found[name])
2849
- throw new ContractError("invalid");
2880
+ const previous = found[name];
2881
+ if (previous)
2882
+ unusable(`sets ${key} twice (lines ${previous.line} and ${at / 2 + 1}); keep one`);
2850
2883
  let value = line.slice(separator + 1).trim();
2851
2884
  if (value[0] === '"' || value[0] === "'")
2852
2885
  value = value.slice(1, -1);
2853
- found[name] = { at, value };
2886
+ found[name] = { at, line: at / 2 + 1, value };
2854
2887
  }
2855
2888
  return { parts, ...found };
2856
2889
  }
2857
- function decodeServerConfig(source) {
2890
+ function decode(source) {
2858
2891
  const { port, token } = settings(source);
2859
- if (!port || !/^[1-9][0-9]{0,4}$/.test(port.value))
2860
- throw new ContractError("invalid");
2892
+ if (!port)
2893
+ unusable(`has no ${PORT_KEY} line`);
2861
2894
  if (!token)
2862
- throw new ContractError("invalid");
2863
- return {
2864
- port: integer(Number(port.value), 1, 65535),
2865
- token: accessToken(token.value)
2866
- };
2895
+ unusable(`has no ${TOKEN_KEY} line`);
2896
+ const p = port, t = token;
2897
+ const number = Number(p.value);
2898
+ if (!/^[1-9][0-9]{0,4}$/.test(p.value) || number > 65535)
2899
+ unusable(`has ${PORT_KEY} on line ${p.line} that is not a whole number from 1 to 65535`);
2900
+ try {
2901
+ return { port: number, token: accessToken(t.value) };
2902
+ } catch {
2903
+ return unusable(`has ${TOKEN_KEY} on line ${t.line} shorter than 32 characters or with control characters`);
2904
+ }
2867
2905
  }
2868
-
2869
- class ServerConfigRefusal extends ContractError {
2870
- path;
2871
- constructor(path2) {
2872
- super("corrupt-selected");
2873
- this.path = path2;
2906
+ var code = (error) => error?.code ?? "error";
2907
+ function locate(path2) {
2908
+ let real;
2909
+ try {
2910
+ real = realpathSync2(path2);
2911
+ } catch (error) {
2912
+ if (code(error) !== "ENOENT")
2913
+ unusable(`cannot be read (${code(error)})`);
2914
+ let link = false;
2915
+ try {
2916
+ link = lstatSync3(path2).isSymbolicLink();
2917
+ } catch {}
2918
+ return unusable(link ? "is a link to a file that does not exist" : "does not exist");
2919
+ }
2920
+ return { real, files: new PrivateFiles(dirname3(real), false) };
2921
+ }
2922
+ function explain(real) {
2923
+ const uid = process.getuid?.();
2924
+ try {
2925
+ const st = lstatSync3(real);
2926
+ if (!st.isFile())
2927
+ return "is not a regular file";
2928
+ if (uid !== undefined && st.uid !== uid)
2929
+ return "is not owned by you";
2930
+ if (uid !== undefined && lstatSync3(dirname3(real)).uid !== uid)
2931
+ return `is in ${dirname3(real)}, which is not owned by you`;
2932
+ if (st.nlink !== 1)
2933
+ return "has more than one hard link";
2934
+ if (st.size > CONFIG_BYTES)
2935
+ return "is larger than 64 KiB";
2936
+ return "changed while Cadence read it; try again";
2937
+ } catch (error) {
2938
+ return `cannot be read (${code(error)})`;
2874
2939
  }
2875
2940
  }
2941
+ function load(path2) {
2942
+ const { real, files } = locate(path2);
2943
+ let snapshot;
2944
+ try {
2945
+ snapshot = files.read(real, CONFIG_BYTES);
2946
+ } catch {
2947
+ return unusable(explain(real));
2948
+ }
2949
+ if (!snapshot)
2950
+ return unusable("does not exist");
2951
+ let source;
2952
+ try {
2953
+ source = decoded(snapshot);
2954
+ } catch {
2955
+ return unusable("is not UTF-8 text");
2956
+ }
2957
+ return { path: path2, real, files, snapshot, source };
2958
+ }
2876
2959
  function decoded(snapshot) {
2877
2960
  return new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes);
2878
2961
  }
2879
- function readServerConfig(path2) {
2962
+ function refusing(path2, read) {
2880
2963
  try {
2881
- const files = new PrivateFiles(dirname3(path2));
2882
- const snapshot = files.read(path2, CONFIG_BYTES);
2883
- if (!snapshot)
2884
- throw new ContractError("corrupt-selected");
2885
- return {
2886
- path: path2,
2887
- config: decodeServerConfig(decoded(snapshot)),
2888
- files,
2889
- snapshot
2890
- };
2891
- } catch {
2892
- throw new ServerConfigRefusal(path2);
2964
+ return read();
2965
+ } catch (error) {
2966
+ if (error instanceof ServerConfigRefusal)
2967
+ throw error;
2968
+ throw new ServerConfigRefusal(path2, error instanceof Unusable ? error.message : `cannot be read (${code(error)})`);
2893
2969
  }
2894
2970
  }
2971
+ function readServerConfig(path2) {
2972
+ return refusing(path2, () => {
2973
+ const file = load(path2);
2974
+ return { ...file, config: decode(file.source) };
2975
+ });
2976
+ }
2895
2977
  function ensureServerConfig(home) {
2896
2978
  const path2 = serverConfigPath(home);
2897
- const added = [];
2898
- try {
2899
- ensureUserConfig(home);
2900
- const files = new PrivateFiles(dirname3(path2));
2901
- const snapshot = files.read(path2, CONFIG_BYTES);
2902
- if (!snapshot)
2903
- throw new ContractError("corrupt-selected");
2904
- const source = decoded(snapshot);
2905
- const found = settings(source);
2979
+ const added = refusing(path2, () => {
2980
+ try {
2981
+ ensureUserConfig(home);
2982
+ } catch (error) {
2983
+ unusable(`could not be created (${code(error)})`);
2984
+ }
2985
+ const file2 = load(path2);
2986
+ const found = settings(file2.source);
2906
2987
  const lines = [];
2907
2988
  if (!found.port)
2908
2989
  lines.push(`${PORT_KEY}=${DEFAULT_PORT}`);
2909
2990
  if (!found.token)
2910
2991
  lines.push(`${TOKEN_KEY}=${randomBytes(32).toString("base64url")}`);
2911
2992
  if (lines.length) {
2993
+ const { source } = file2;
2912
2994
  const eol = source.includes(`\r
2913
2995
  `) ? `\r
2914
2996
  ` : `
2915
2997
  `;
2916
2998
  const ending = !source || /[\r\n]$/.test(source) ? "" : eol;
2917
2999
  const gap = source ? eol : "";
2918
- files.replace(path2, snapshot, `${source}${ending}${gap}${[...HEADER, ...lines].join(eol)}${eol}`, CONFIG_BYTES);
2919
- added.push(...lines.map((line) => line.slice(0, line.indexOf("="))));
3000
+ try {
3001
+ file2.files.replace(file2.real, file2.snapshot, `${source}${ending}${gap}${[...HEADER, ...lines].join(eol)}${eol}`, CONFIG_BYTES);
3002
+ } catch (error) {
3003
+ unusable(`could not be updated (${code(error)})`);
3004
+ }
2920
3005
  }
2921
- } catch {
2922
- throw new ServerConfigRefusal(path2);
2923
- }
2924
- return { ...readServerConfig(path2), added };
3006
+ return lines.map((line) => line.slice(0, line.indexOf("=")));
3007
+ });
3008
+ const file = readServerConfig(path2);
3009
+ const exposed = process.platform !== "win32" && (statSync(file.real).mode & 63) !== 0;
3010
+ return { ...file, added, exposed };
2925
3011
  }
2926
3012
 
2927
3013
  // src/service/autostart.ts
@@ -3000,7 +3086,7 @@ function agentDirectory(s) {
3000
3086
  function piUserInstall(s) {
3001
3087
  try {
3002
3088
  const managed = join4(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
3003
- return realpathSync2(managed) === realpathSync2(s.root);
3089
+ return realpathSync3(managed) === realpathSync3(s.root);
3004
3090
  } catch {
3005
3091
  return false;
3006
3092
  }
@@ -3008,7 +3094,7 @@ function piUserInstall(s) {
3008
3094
  function sdkPackages(root) {
3009
3095
  let start;
3010
3096
  try {
3011
- start = realpathSync2(join4(root, "src"));
3097
+ start = realpathSync3(join4(root, "src"));
3012
3098
  } catch {
3013
3099
  return;
3014
3100
  }
@@ -3089,7 +3175,7 @@ function buildUnit(u) {
3089
3175
  }
3090
3176
  function readGenerated(path2, marker) {
3091
3177
  try {
3092
- if (!lstatSync3(path2).isFile())
3178
+ if (!lstatSync4(path2).isFile())
3093
3179
  return null;
3094
3180
  const bytes = readFileSync(path2);
3095
3181
  const text2 = bytes[0] === 255 && bytes[1] === 254 ? bytes.subarray(2).toString("utf16le") : bytes.toString("utf8");
@@ -3105,7 +3191,7 @@ function unitWorkspace(text2) {
3105
3191
  }
3106
3192
  function directory(path2) {
3107
3193
  try {
3108
- return isAbsolute3(path2) && statSync(path2).isDirectory();
3194
+ return isAbsolute3(path2) && statSync2(path2).isDirectory();
3109
3195
  } catch {
3110
3196
  return false;
3111
3197
  }
@@ -3140,7 +3226,7 @@ function write(path2, content) {
3140
3226
  }
3141
3227
  function regularFile(path2) {
3142
3228
  try {
3143
- return statSync(path2).isFile();
3229
+ return statSync2(path2).isFile();
3144
3230
  } catch {
3145
3231
  return false;
3146
3232
  }
@@ -3270,7 +3356,7 @@ function autostartStatus(s) {
3270
3356
 
3271
3357
  // src/service/coordination.ts
3272
3358
  import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
3273
- import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
3359
+ import { opendirSync, realpathSync as realpathSync4 } from "node:fs";
3274
3360
  import { hostname as hostname2 } from "node:os";
3275
3361
  import { dirname as dirname5, isAbsolute as isAbsolute4, join as join5, relative as relative3, sep as sep4 } from "node:path";
3276
3362
  var registrationKeys = [
@@ -3428,7 +3514,7 @@ class ServiceCoordination {
3428
3514
  const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
3429
3515
  const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
3430
3516
  for (const [i, row] of entries.entries()) {
3431
- if (row.stateHome !== this.stateHome || realpathSync3(row.root) !== row.root)
3517
+ if (row.stateHome !== this.stateHome || realpathSync4(row.root) !== row.root)
3432
3518
  refused();
3433
3519
  if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
3434
3520
  refused();
@@ -4167,7 +4253,7 @@ import { isatty } from "node:tty";
4167
4253
 
4168
4254
  // src/history/recovery.ts
4169
4255
  import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
4170
- import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
4256
+ import { lstatSync as lstatSync7, opendirSync as opendirSync3 } from "node:fs";
4171
4257
  import { join as join9, relative as relative4 } from "node:path";
4172
4258
 
4173
4259
  // src/service/recovery.ts
@@ -4222,7 +4308,7 @@ import {
4222
4308
  constants as constants5,
4223
4309
  fstatSync as fstatSync4,
4224
4310
  fsyncSync as fsyncSync3,
4225
- lstatSync as lstatSync5,
4311
+ lstatSync as lstatSync6,
4226
4312
  opendirSync as opendirSync2,
4227
4313
  openSync as openSync5,
4228
4314
  statfsSync
@@ -4252,7 +4338,7 @@ import {
4252
4338
  closeSync as closeSync4,
4253
4339
  constants as constants4,
4254
4340
  fstatSync as fstatSync3,
4255
- lstatSync as lstatSync4,
4341
+ lstatSync as lstatSync5,
4256
4342
  openSync as openSync4,
4257
4343
  readSync as readSync3
4258
4344
  } from "node:fs";
@@ -4264,7 +4350,7 @@ var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.
4264
4350
  function selected(files, path2, read, walks) {
4265
4351
  if (!files.files.directory(dirname6(path2), false, walks))
4266
4352
  bad();
4267
- const before = lstatSync4(path2);
4353
+ const before = lstatSync5(path2);
4268
4354
  if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
4269
4355
  bad();
4270
4356
  const fd = openSync4(path2, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
@@ -4272,7 +4358,7 @@ function selected(files, path2, read, walks) {
4272
4358
  if (stamp2(fstatSync3(fd)) !== stamp2(before))
4273
4359
  bad();
4274
4360
  const result = read(fd, before.size, stamp2(before));
4275
- if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(path2)) !== stamp2(before))
4361
+ if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync5(path2)) !== stamp2(before))
4276
4362
  bad();
4277
4363
  return result;
4278
4364
  } finally {
@@ -4900,7 +4986,7 @@ class HistoryFiles {
4900
4986
  return this.files.read(path2, maximum);
4901
4987
  let size;
4902
4988
  try {
4903
- size = lstatSync5(path2).size;
4989
+ size = lstatSync6(path2).size;
4904
4990
  } catch {
4905
4991
  return this.files.read(path2, 0, budget.walks);
4906
4992
  }
@@ -4988,7 +5074,7 @@ class HistoryFiles {
4988
5074
  const dir = opendirSync2(path2);
4989
5075
  try {
4990
5076
  for (let e = dir.readSync();e; e = dir.readSync()) {
4991
- const p = join8(path2, e.name), st = lstatSync5(p);
5077
+ const p = join8(path2, e.name), st = lstatSync6(p);
4992
5078
  if (st.isDirectory())
4993
5079
  bytes += walk(p, depth + 1);
4994
5080
  else {
@@ -5188,7 +5274,7 @@ class HistoryFiles {
5188
5274
  let committed;
5189
5275
  const fd = await open(path2, constants5.O_WRONLY | constants5.O_APPEND | (constants5.O_NOFOLLOW ?? 0));
5190
5276
  const assertOriginal = (bytes) => {
5191
- const original = fstatSync4(fd.fd), selected2 = lstatSync5(path2);
5277
+ const original = fstatSync4(fd.fd), selected2 = lstatSync6(path2);
5192
5278
  if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
5193
5279
  corrupt();
5194
5280
  const snapshot = this.files.read(path2, LIMITS.segmentBytes);
@@ -5305,7 +5391,7 @@ class HistoryFiles {
5305
5391
  try {
5306
5392
  lease.assert();
5307
5393
  this.files.assert(path2, before, LIMITS.segmentBytes);
5308
- const opened = await fd.stat(), selected2 = lstatSync5(path2);
5394
+ const opened = await fd.stat(), selected2 = lstatSync6(path2);
5309
5395
  if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
5310
5396
  corrupt();
5311
5397
  beforeEffect?.();
@@ -5753,7 +5839,7 @@ function digestHistory(files, exclude) {
5753
5839
  const walk = (path2, depth) => {
5754
5840
  if (depth > 8 || !files.files.directory(path2))
5755
5841
  throw new ContractError("corrupt-selected");
5756
- const directory2 = lstatSync6(path2);
5842
+ const directory2 = lstatSync7(path2);
5757
5843
  digest.update(`incarnation:${directory2.dev}:${directory2.ino}
5758
5844
  `);
5759
5845
  const dir = opendirSync3(path2), names2 = [];
@@ -5771,7 +5857,7 @@ function digestHistory(files, exclude) {
5771
5857
  if (selected2 === exclude)
5772
5858
  continue;
5773
5859
  const rel = relative4(files.directory, selected2);
5774
- if (lstatSync6(selected2).isDirectory()) {
5860
+ if (lstatSync7(selected2).isDirectory()) {
5775
5861
  digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
5776
5862
  `);
5777
5863
  walk(selected2, depth + 1);
@@ -6395,8 +6481,9 @@ async function stopService(c) {
6395
6481
  function failedStatus(result) {
6396
6482
  return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
6397
6483
  }
6398
- function serverConfigHint(path2) {
6399
- return `Cadence changed nothing: ${path2} must be your own private regular file (chmod 600, in a chmod 700 directory on Linux and macOS) in KEY=value lines with exactly one ${PORT_KEY}=<1-65535> and one ${TOKEN_KEY}=<at least 32 characters>.
6484
+ function serverConfigHint(error) {
6485
+ return `Cadence changed nothing: ${error.path} ${error.reason}.
6486
+ Fix it and run the command again; the file holds the WebUI port and token as ${PORT_KEY}=<1-65535> and ${TOKEN_KEY}=<at least 32 characters>, one line each.
6400
6487
  `;
6401
6488
  }
6402
6489
  function startupGuidance(result, input) {
@@ -6438,7 +6525,7 @@ async function startService(options, foreground, hostUrl) {
6438
6525
  input = decodeHostInput(selectStart(options, process.env));
6439
6526
  } catch (error) {
6440
6527
  if (error instanceof ServerConfigRefusal)
6441
- process.stderr.write(serverConfigHint(error.path));
6528
+ process.stderr.write(serverConfigHint(error));
6442
6529
  throw error;
6443
6530
  }
6444
6531
  const serialized = `${JSON.stringify(input)}
@@ -6466,9 +6553,9 @@ async function startService(options, foreground, hostUrl) {
6466
6553
  stdio: ["pipe", "pipe", "ignore"]
6467
6554
  });
6468
6555
  let completed = false;
6469
- const closure = new Promise((resolve6) => child.once("close", (code, signal) => {
6556
+ const closure = new Promise((resolve6) => child.once("close", (code2, signal) => {
6470
6557
  completed = true;
6471
- if (foreground && (code !== 0 || signal !== null))
6558
+ if (foreground && (code2 !== 0 || signal !== null))
6472
6559
  process.exitCode = 1;
6473
6560
  resolve6();
6474
6561
  }));
@@ -6752,13 +6839,17 @@ async function serverMain(argv, hostUrl) {
6752
6839
  process.stderr.write(researchTemplate(s.home));
6753
6840
  if (action === "install" || action === "restart") {
6754
6841
  try {
6755
- const { added } = ensureServerConfig(s.home);
6842
+ const { added, exposed } = ensureServerConfig(s.home);
6843
+ const path2 = serverConfigPath(s.home);
6756
6844
  if (added.length)
6757
- process.stderr.write(`Cadence added ${added.map((key) => key === PORT_KEY ? `${key}=${DEFAULT_PORT}` : `a random private ${key}`).join(" and ")} to ${serverConfigPath(s.home)}
6845
+ process.stderr.write(`Cadence added ${added.map((key) => key === PORT_KEY ? `${key}=${DEFAULT_PORT}` : `a random private ${key}`).join(" and ")} to ${path2}
6846
+ `);
6847
+ if (exposed)
6848
+ process.stderr.write(`Note: other users can read ${path2}; chmod 600 ${JSON.stringify(path2)} keeps ${TOKEN_KEY} private.
6758
6849
  `);
6759
6850
  } catch (error) {
6760
6851
  if (error instanceof ServerConfigRefusal)
6761
- process.stderr.write(serverConfigHint(error.path));
6852
+ process.stderr.write(serverConfigHint(error));
6762
6853
  throw error;
6763
6854
  }
6764
6855
  }