@abelxiaoxing/cadence 2.0.6 → 2.0.7

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.
@@ -5,7 +5,7 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { existsSync, mkdirSync as mkdirSync5 } from "node:fs";
7
7
  import { request } from "node:http";
8
- import { dirname as dirname8, join as join10, resolve as resolve5 } from "node:path";
8
+ import { dirname as dirname9, join as join11, resolve as resolve5 } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  // src/install-config.mjs
@@ -2511,372 +2511,61 @@ 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 lstatSync2,
2515
- mkdirSync as mkdirSync3,
2514
+ lstatSync as lstatSync3,
2515
+ mkdirSync as mkdirSync4,
2516
2516
  readFileSync,
2517
2517
  realpathSync as realpathSync2,
2518
- renameSync as renameSync2,
2518
+ renameSync as renameSync3,
2519
2519
  rmSync,
2520
2520
  statSync,
2521
- writeFileSync as writeFileSync2
2521
+ writeFileSync as writeFileSync3
2522
2522
  } from "node:fs";
2523
2523
  import { homedir as homedir3 } from "node:os";
2524
2524
  import {
2525
2525
  delimiter,
2526
- dirname as dirname2,
2527
- isAbsolute as isAbsolute2,
2528
- join as join2,
2529
- resolve as resolve2,
2530
- sep as sep2,
2526
+ dirname as dirname4,
2527
+ isAbsolute as isAbsolute3,
2528
+ join as join4,
2529
+ resolve as resolve3,
2530
+ sep as sep3,
2531
2531
  win32
2532
2532
  } from "node:path";
2533
- var UNIT = "cadence.service";
2534
- var MARKER = "# Generated by @abelxiaoxing/cadence";
2535
- function wscript(env) {
2536
- const root = env.SystemRoot;
2537
- return win32.join(root && win32.isAbsolute(root) ? root : "C:\\Windows", "System32", "wscript.exe");
2538
- }
2539
- function hostSystem(root, owner) {
2540
- const run = (command, args) => {
2541
- const r = spawnSync2(command, args, {
2542
- encoding: "utf8",
2543
- stdio: ["ignore", "pipe", "ignore"],
2544
- windowsHide: true
2545
- });
2546
- return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
2547
- };
2548
- return {
2549
- platform: process.platform,
2550
- env: process.env,
2551
- home: homedir3(),
2552
- node: process.execPath,
2553
- root,
2554
- unit: UNIT,
2555
- uid: process.getuid?.(),
2556
- systemctl: (args) => run("systemctl", ["--user", ...args]),
2557
- linger: () => {
2558
- const r = run("loginctl", [
2559
- "show-user",
2560
- String(process.getuid?.()),
2561
- "--property=Linger",
2562
- "--value"
2563
- ]);
2564
- return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
2565
- },
2566
- enableLinger: (interactive) => {
2567
- const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
2568
- "--system",
2569
- "--allow-interactive-authorization=no",
2570
- "--timeout=15",
2571
- "call",
2572
- "org.freedesktop.login1",
2573
- "/org/freedesktop/login1",
2574
- "org.freedesktop.login1.Manager",
2575
- "SetUserLinger",
2576
- "ubb",
2577
- String(process.getuid?.()),
2578
- "true",
2579
- "false"
2580
- ], { stdio: "ignore", timeout: 20000 });
2581
- return !r.error && r.status === 0;
2582
- },
2583
- launchctl: (args) => run("launchctl", args),
2584
- reg: (args) => run("reg", args),
2585
- launch: (path2) => {
2586
- const r = spawnSync2(wscript(process.env), [path2], {
2587
- stdio: "ignore",
2588
- windowsHide: true,
2589
- timeout: 20000
2590
- });
2591
- return !r.error && r.status === 0;
2592
- },
2593
- ...owner
2594
- };
2595
- }
2596
- function expandHome(value, home) {
2597
- if (value === "~")
2598
- return home;
2599
- return value.startsWith("~/") ? join2(home, value.slice(2)) : value;
2600
- }
2601
- function agentDirectory(s) {
2602
- const selected = s.env.PI_CODING_AGENT_DIR;
2603
- return resolve2(selected ? expandHome(selected, s.home) : join2(s.home, ".pi", "agent"));
2604
- }
2605
- function piUserInstall(s) {
2606
- try {
2607
- const managed = join2(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
2608
- return realpathSync2(managed) === realpathSync2(s.root);
2609
- } catch {
2610
- return false;
2611
- }
2612
- }
2613
- function sdkPackages(root) {
2614
- let start;
2615
- try {
2616
- start = realpathSync2(join2(root, "src"));
2617
- } catch {
2618
- return;
2619
- }
2620
- const found = [];
2621
- for (const name of ["pi-coding-agent", "pi-ai"]) {
2622
- let hit;
2623
- for (let dir = start;!hit; dir = dirname2(dir)) {
2624
- const candidate = join2(dir, "node_modules", "@earendil-works", name, "package.json");
2625
- if (regularFile(candidate))
2626
- hit = candidate;
2627
- else if (dirname2(dir) === dir)
2628
- return;
2629
- }
2630
- found.push(hit);
2631
- }
2632
- return found;
2633
- }
2634
- function unitPath(s) {
2635
- const data = s.env.XDG_DATA_HOME;
2636
- return join2(data && isAbsolute2(data) ? data : join2(s.home, ".local", "share"), "systemd", "user", s.unit);
2637
- }
2638
- function servicePath(value) {
2639
- const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute2(entry) && !entry.endsWith(`${sep2}node_modules${sep2}.bin`) && !entry.includes(`${sep2}node-gyp-bin`));
2640
- return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
2641
- }
2642
- function serviceEnvironment(s) {
2643
- const out = [["PATH", servicePath(s.env.PATH)]];
2644
- for (const key of ["LANG", "LC_ALL"]) {
2645
- const value = s.env[key];
2646
- if (value)
2647
- out.push([key, value]);
2648
- }
2649
- const state = s.env.XDG_STATE_HOME;
2650
- if (state && isAbsolute2(state))
2651
- out.push(["XDG_STATE_HOME", state]);
2652
- if (s.env.PI_CODING_AGENT_DIR)
2653
- out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
2654
- return out;
2655
- }
2656
- function checked(value) {
2657
- if (/[\u0000-\u001f\u007f]/.test(value))
2658
- throw new ContractError("invalid");
2659
- return value;
2660
- }
2661
- var specifiers = (value) => checked(value).replaceAll("%", "%%");
2662
- var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
2663
- var argument = (value) => quoted(value).replaceAll("$", "$$$$");
2664
- function buildUnit(u) {
2665
- const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
2666
- return [
2667
- `${MARKER}; \`cadence server install\` and \`restart\` rewrite this file.`,
2668
- "# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
2669
- "[Unit]",
2670
- "Description=Cadence shared task service and WebUI",
2671
- "Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
2672
- `ConditionPathExists=${specifiers(u.cli)}`,
2673
- `ConditionPathExists=${specifiers(join2(u.agentDir, "auth.json"))}`,
2674
- ...u.sdk.map((path2) => `ConditionPathExists=${specifiers(path2)}`),
2675
- "StartLimitIntervalSec=300",
2676
- "StartLimitBurst=3",
2677
- "",
2678
- "[Service]",
2679
- "Type=simple",
2680
- `WorkingDirectory=${specifiers(u.workspace)}`,
2681
- ...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
2682
- `ExecStart=${command(["start", "--foreground", "--workspace", u.workspace])}`,
2683
- `ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
2684
- "KillMode=mixed",
2685
- "TimeoutStopSec=90",
2686
- "Restart=on-failure",
2687
- "RestartSec=10",
2688
- "",
2689
- "[Install]",
2690
- "WantedBy=default.target",
2691
- ""
2692
- ].join(`
2533
+
2534
+ // src/service/server-config.ts
2535
+ import { randomBytes } from "node:crypto";
2536
+ import { dirname as dirname3, join as join3 } from "node:path";
2537
+
2538
+ // skills/_shared/load-config.mjs
2539
+ function parseEnvFile(content) {
2540
+ const values = {};
2541
+ const lines = content.replaceAll(`\r
2542
+ `, `
2543
+ `).replaceAll("\r", `
2544
+ `).split(`
2693
2545
  `);
2694
- }
2695
- function readGenerated(path2, marker) {
2696
- try {
2697
- if (!lstatSync2(path2).isFile())
2698
- return null;
2699
- const bytes = readFileSync(path2);
2700
- const text2 = bytes[0] === 255 && bytes[1] === 254 ? bytes.subarray(2).toString("utf16le") : bytes.toString("utf8");
2701
- return text2.startsWith(marker) ? text2 : null;
2702
- } catch (error) {
2703
- if (error.code === "ENOENT")
2704
- return;
2705
- throw error;
2706
- }
2707
- }
2708
- function unitWorkspace(text2) {
2709
- return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
2710
- }
2711
- function directory(path2) {
2712
- try {
2713
- return isAbsolute2(path2) && statSync(path2).isDirectory();
2714
- } catch {
2715
- return false;
2716
- }
2717
- }
2718
- function serviceStateHome(s) {
2719
- const state = s.platform === "win32" ? s.env.LOCALAPPDATA : s.env.XDG_STATE_HOME;
2720
- return state && isAbsolute2(state) ? state : join2(s.home, ...s.platform === "win32" ? ["AppData", "Local"] : [".local", "state"]);
2721
- }
2722
- function selectWorkspace(s, requested, kept) {
2723
- let chosen;
2724
- if (requested) {
2725
- chosen = expandHome(requested, s.home);
2726
- if (!directory(chosen))
2727
- throw new ContractError("invalid");
2728
- chosen = resolve2(chosen);
2729
- } else if (kept && directory(kept))
2730
- chosen = kept;
2731
- const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace: join2(s.home, LANDING_WORKSPACE), redirected: false };
2732
- if (landing.workspace !== chosen)
2733
- mkdirSync3(landing.workspace, { recursive: true });
2734
- return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
2735
- }
2736
- function write(path2, content) {
2737
- mkdirSync3(dirname2(path2), { recursive: true });
2738
- const temporary = `${path2}.${process.pid}.tmp`;
2739
- try {
2740
- writeFileSync2(temporary, content, { mode: 420, flag: "wx" });
2741
- renameSync2(temporary, path2);
2742
- } finally {
2743
- rmSync(temporary, { force: true });
2744
- }
2745
- }
2746
- function regularFile(path2) {
2747
- try {
2748
- return statSync(path2).isFile();
2749
- } catch {
2750
- return false;
2546
+ for (let index = 0;index < lines.length; index += 1) {
2547
+ const line = lines[index].trim();
2548
+ if (!line || line.startsWith("#"))
2549
+ continue;
2550
+ const separator = line.indexOf("=");
2551
+ if (separator <= 0)
2552
+ throw new Error(`Invalid configuration syntax at line ${index + 1}`);
2553
+ const name = line.slice(0, separator).trim();
2554
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2555
+ throw new Error(`Invalid configuration name at line ${index + 1}`);
2556
+ }
2557
+ let value = line.slice(separator + 1).trim();
2558
+ const quote = value[0];
2559
+ if (quote === '"' || quote === "'") {
2560
+ if (value.at(-1) !== quote) {
2561
+ throw new Error(`Unterminated quoted configuration value at line ${index + 1}`);
2562
+ }
2563
+ value = value.slice(1, -1);
2564
+ }
2565
+ values[name] = value;
2751
2566
  }
2567
+ return values;
2752
2568
  }
2753
- async function settle(s, workspace) {
2754
- if (!workspace)
2755
- return;
2756
- const status = await s.stopOwner(workspace);
2757
- if (status === "stopped")
2758
- return;
2759
- return {
2760
- status: status === "quarantined" || status === "unavailable" ? status : "unconfirmed",
2761
- workspace
2762
- };
2763
- }
2764
- function bootStart(s, attempt) {
2765
- const current = s.linger();
2766
- if (current === undefined)
2767
- return {};
2768
- if (current)
2769
- return { linger: "on" };
2770
- if (!attempt)
2771
- return { linger: "off" };
2772
- return {
2773
- linger: s.enableLinger(false) || s.enableLinger(true) ? "enabled" : "refused"
2774
- };
2775
- }
2776
- function configureAutostart(s, mode, requested, linger = false) {
2777
- if (s.platform !== "linux")
2778
- return { status: "unsupported" };
2779
- if (s.systemctl(["show-environment"]).status !== 0)
2780
- return { status: "unavailable" };
2781
- const path2 = unitPath(s);
2782
- const previous = readGenerated(path2, MARKER);
2783
- if (previous === null)
2784
- return { status: "foreign", unit: path2 };
2785
- if (mode === "restart" && previous === undefined)
2786
- return { status: "absent" };
2787
- const sdk = sdkPackages(s.root);
2788
- if (!sdk)
2789
- return { status: "missing-pi-sdk" };
2790
- const { workspace, redirectedFrom } = selectWorkspace(s, requested, unitWorkspace(previous));
2791
- const agentDir = agentDirectory(s);
2792
- const content = buildUnit({
2793
- node: s.node,
2794
- cli: join2(s.root, "src", "service-cli.mjs"),
2795
- workspace,
2796
- agentDir,
2797
- sdk,
2798
- environment: serviceEnvironment(s)
2799
- });
2800
- const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
2801
- const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
2802
- if (active)
2803
- s.systemctl(["stop", s.unit]);
2804
- write(path2, content);
2805
- s.systemctl(["daemon-reload"]);
2806
- const result = {
2807
- unit: path2,
2808
- workspace,
2809
- ...redirectedFrom ? { redirectedFrom } : {}
2810
- };
2811
- if (enabled.startsWith("masked"))
2812
- return { status: "masked", ...result };
2813
- if (mode === "install" && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
2814
- return { status: "failed", ...result };
2815
- const boot = () => mode === "install" ? bootStart(s, linger) : {};
2816
- if (!regularFile(join2(agentDir, "auth.json")))
2817
- return { status: "waiting-for-pi-login", ...result, ...boot() };
2818
- s.systemctl(["reset-failed", s.unit]);
2819
- const started = s.systemctl(["start", s.unit]).status === 0;
2820
- return { status: started ? "started" : "failed", ...result, ...boot() };
2821
- }
2822
- function stopAutostart(s) {
2823
- if (s.platform !== "linux")
2824
- return { status: "unsupported" };
2825
- const path2 = unitPath(s);
2826
- const previous = readGenerated(path2, MARKER);
2827
- if (previous === null)
2828
- return { status: "foreign", unit: path2 };
2829
- if (previous === undefined)
2830
- return { status: "absent" };
2831
- if (s.systemctl(["show-environment"]).status !== 0)
2832
- return { status: "unavailable", unit: path2 };
2833
- return {
2834
- status: s.systemctl(["stop", s.unit]).status === 0 ? "stopped" : "failed",
2835
- unit: path2,
2836
- workspace: unitWorkspace(previous)
2837
- };
2838
- }
2839
- function removeAutostart(s) {
2840
- if (s.platform !== "linux")
2841
- return { status: "unsupported" };
2842
- const path2 = unitPath(s);
2843
- const previous = readGenerated(path2, MARKER);
2844
- if (previous === null)
2845
- return { status: "foreign", unit: path2 };
2846
- if (previous === undefined)
2847
- return { status: "absent" };
2848
- if (s.systemctl(["show-environment"]).status !== 0)
2849
- return { status: "unavailable", unit: path2 };
2850
- if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
2851
- return { status: "failed", unit: path2 };
2852
- rmSync(path2);
2853
- s.systemctl(["daemon-reload"]);
2854
- s.systemctl(["reset-failed", s.unit]);
2855
- return { status: "removed", unit: path2 };
2856
- }
2857
- function autostartStatus(s) {
2858
- if (s.platform !== "linux")
2859
- return { status: "unsupported" };
2860
- const path2 = unitPath(s);
2861
- const previous = readGenerated(path2, MARKER);
2862
- if (previous === null)
2863
- return { status: "foreign", unit: path2 };
2864
- if (previous === undefined)
2865
- return { status: "absent" };
2866
- return {
2867
- status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
2868
- enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
2869
- unit: path2,
2870
- workspace: unitWorkspace(previous),
2871
- ...bootStart(s, false)
2872
- };
2873
- }
2874
-
2875
- // src/service/coordination.ts
2876
- import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
2877
- import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
2878
- import { hostname as hostname2 } from "node:os";
2879
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join4, relative as relative3, sep as sep4 } from "node:path";
2880
2569
 
2881
2570
  // src/service/private-files.ts
2882
2571
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
@@ -2885,15 +2574,15 @@ import {
2885
2574
  constants as constants3,
2886
2575
  fstatSync as fstatSync2,
2887
2576
  fsyncSync as fsyncSync2,
2888
- lstatSync as lstatSync3,
2889
- mkdirSync as mkdirSync4,
2577
+ lstatSync as lstatSync2,
2578
+ mkdirSync as mkdirSync3,
2890
2579
  openSync as openSync3,
2891
2580
  readSync as readSync2,
2892
- renameSync as renameSync3,
2581
+ renameSync as renameSync2,
2893
2582
  unlinkSync as unlinkSync2,
2894
- writeFileSync as writeFileSync3
2583
+ writeFileSync as writeFileSync2
2895
2584
  } from "node:fs";
2896
- import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
2585
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
2897
2586
  var METADATA_BYTES = 16 * 1024;
2898
2587
  var REGISTRY_BYTES = 1024 * 1024;
2899
2588
  var REGISTRY_ENTRIES = 128;
@@ -2945,7 +2634,7 @@ class DirectoryWalks {
2945
2634
  return false;
2946
2635
  let st;
2947
2636
  try {
2948
- st = lstatSync3(path2);
2637
+ st = lstatSync2(path2);
2949
2638
  } catch (error) {
2950
2639
  if (!missing(error))
2951
2640
  throw error;
@@ -2970,13 +2659,13 @@ class PrivateFiles {
2970
2659
  }
2971
2660
  directory(path2, create = false, walks) {
2972
2661
  const tail = relative2(this.namespace, path2);
2973
- if (isAbsolute3(tail) || tail === ".." || tail.startsWith(`..${sep3}`))
2662
+ if (isAbsolute2(tail) || tail === ".." || tail.startsWith(`..${sep2}`))
2974
2663
  refused();
2975
2664
  if (!create) {
2976
- const full = resolve3(path2);
2665
+ const full = resolve2(path2);
2977
2666
  if (walks?.reuse(full, this.ownerOnly))
2978
2667
  return true;
2979
- const namespace = resolve3(this.namespace);
2668
+ const namespace = resolve2(this.namespace);
2980
2669
  let inside = false, last;
2981
2670
  const found = safeDirectory(path2, false, (at2, st) => {
2982
2671
  if (at2 === namespace)
@@ -2991,23 +2680,23 @@ class PrivateFiles {
2991
2680
  }
2992
2681
  if (!safeDirectory(this.namespace, true))
2993
2682
  return false;
2994
- privateMode(lstatSync3(this.namespace), this.ownerOnly);
2683
+ privateMode(lstatSync2(this.namespace), this.ownerOnly);
2995
2684
  let at = this.namespace;
2996
- for (const part of tail.split(sep3).filter(Boolean)) {
2997
- at = join3(at, part);
2685
+ for (const part of tail.split(sep2).filter(Boolean)) {
2686
+ at = join2(at, part);
2998
2687
  let st;
2999
2688
  try {
3000
- st = lstatSync3(at);
2689
+ st = lstatSync2(at);
3001
2690
  } catch (error) {
3002
2691
  if (!missing(error))
3003
2692
  throw error;
3004
2693
  try {
3005
- mkdirSync4(at, { mode: 448 });
2694
+ mkdirSync3(at, { mode: 448 });
3006
2695
  } catch (mkdirError) {
3007
2696
  if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
3008
2697
  throw mkdirError;
3009
2698
  }
3010
- st = lstatSync3(at);
2699
+ st = lstatSync2(at);
3011
2700
  }
3012
2701
  if (!st.isDirectory() || st.isSymbolicLink())
3013
2702
  throw new Error("Unsafe directory");
@@ -3016,11 +2705,11 @@ class PrivateFiles {
3016
2705
  return true;
3017
2706
  }
3018
2707
  read(path2, maximum = METADATA_BYTES, walks) {
3019
- if (!this.directory(dirname3(path2), false, walks))
2708
+ if (!this.directory(dirname2(path2), false, walks))
3020
2709
  return;
3021
2710
  let before;
3022
2711
  try {
3023
- before = lstatSync3(path2);
2712
+ before = lstatSync2(path2);
3024
2713
  } catch (error) {
3025
2714
  if (missing(error))
3026
2715
  return;
@@ -3043,7 +2732,7 @@ class PrivateFiles {
3043
2732
  size += n;
3044
2733
  }
3045
2734
  const bytes = buffer.subarray(0, size);
3046
- if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync3(path2)))
2735
+ if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync2(path2)))
3047
2736
  refused();
3048
2737
  return { bytes, identity: hash(bytes), stamp: stamp(opened) };
3049
2738
  } finally {
@@ -3058,15 +2747,15 @@ class PrivateFiles {
3058
2747
  create(path2, bytes, maximum = METADATA_BYTES) {
3059
2748
  if (Buffer.byteLength(bytes) > maximum)
3060
2749
  throw new ContractError("oversize");
3061
- this.directory(dirname3(path2), true);
2750
+ this.directory(dirname2(path2), true);
3062
2751
  const fd = retryWindowsFileOperation(() => openSync3(path2, "wx", 384));
3063
2752
  try {
3064
- writeFileSync3(fd, bytes);
2753
+ writeFileSync2(fd, bytes);
3065
2754
  fsyncSync2(fd);
3066
2755
  } finally {
3067
2756
  closeSync3(fd);
3068
2757
  }
3069
- this.syncDirectory(dirname3(path2));
2758
+ this.syncDirectory(dirname2(path2));
3070
2759
  const result = this.read(path2, maximum);
3071
2760
  if (!result || result.identity !== hash(bytes))
3072
2761
  refused();
@@ -3074,11 +2763,11 @@ class PrivateFiles {
3074
2763
  }
3075
2764
  replace(path2, expected, bytes, maximum = METADATA_BYTES) {
3076
2765
  this.assert(path2, expected, maximum);
3077
- const temporary = join3(dirname3(path2), `pending-${randomUUID2()}.json`);
2766
+ const temporary = join2(dirname2(path2), `pending-${randomUUID2()}.json`);
3078
2767
  this.create(temporary, bytes, maximum);
3079
2768
  this.assert(path2, expected, maximum);
3080
- retryWindowsFileOperation(() => renameSync3(temporary, path2), () => this.assert(path2, expected, maximum));
3081
- this.syncDirectory(dirname3(path2));
2769
+ retryWindowsFileOperation(() => renameSync2(temporary, path2), () => this.assert(path2, expected, maximum));
2770
+ this.syncDirectory(dirname2(path2));
3082
2771
  const result = this.read(path2, maximum);
3083
2772
  if (!result || result.identity !== hash(bytes))
3084
2773
  refused();
@@ -3087,7 +2776,7 @@ class PrivateFiles {
3087
2776
  remove(path2, expected, maximum = METADATA_BYTES) {
3088
2777
  this.assert(path2, expected, maximum);
3089
2778
  retryWindowsFileOperation(() => unlinkSync2(path2), () => this.assert(path2, expected, maximum));
3090
- this.syncDirectory(dirname3(path2));
2779
+ this.syncDirectory(dirname2(path2));
3091
2780
  }
3092
2781
  syncDirectory(path2) {
3093
2782
  if (process.platform === "win32")
@@ -3100,31 +2789,490 @@ class PrivateFiles {
3100
2789
  }
3101
2790
  }
3102
2791
  }
3103
- function stamp(st) {
2792
+ function stamp(st) {
2793
+ return [
2794
+ st.dev,
2795
+ st.ino,
2796
+ st.size,
2797
+ st.mode,
2798
+ st.uid,
2799
+ st.nlink,
2800
+ st.ctimeMs,
2801
+ st.mtimeMs
2802
+ ].join(":");
2803
+ }
2804
+ function json(snapshot, maximum = METADATA_BYTES) {
2805
+ try {
2806
+ return parseWireJson(new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes), maximum);
2807
+ } catch {
2808
+ return refused();
2809
+ }
2810
+ }
2811
+ function encoded(value) {
2812
+ return `${JSON.stringify(value)}
2813
+ `;
2814
+ }
2815
+
2816
+ // src/service/server-config.ts
2817
+ var DEFAULT_PORT = 17423;
2818
+ var PORT_KEY = "CADENCE_PORT";
2819
+ var TOKEN_KEY = "CADENCE_TOKEN";
2820
+ var CONFIG_BYTES = 64 * 1024;
2821
+ var HEADER = [
2822
+ "# Cadence WebUI for `cadence server install`: edit, then run `cadence server restart`.",
2823
+ "# The token admits anyone who has it to your workspace; keep it private."
2824
+ ];
2825
+ var TOKEN_BYTES = 1024;
2826
+ function accessToken(value) {
2827
+ const result = text(value, TOKEN_BYTES, true);
2828
+ if (utf8Bytes(result) < 32 || /[\p{Cc}\p{Cf}]/u.test(result))
2829
+ throw new ContractError("invalid");
2830
+ return result;
2831
+ }
2832
+ function serverConfigPath(home) {
2833
+ return join3(home, ".pi", "agent", "cadence", ".env");
2834
+ }
2835
+ function settings(source) {
2836
+ parseEnvFile(source);
2837
+ const parts = source.split(/(\r\n|\r|\n)/);
2838
+ const found = {};
2839
+ for (let at = 0;at < parts.length; at += 2) {
2840
+ const line = parts[at].trim();
2841
+ if (!line || line.startsWith("#"))
2842
+ continue;
2843
+ const separator = line.indexOf("=");
2844
+ const key = line.slice(0, separator).trim();
2845
+ const name = key === PORT_KEY ? "port" : key === TOKEN_KEY ? "token" : undefined;
2846
+ if (!name)
2847
+ continue;
2848
+ if (found[name])
2849
+ throw new ContractError("invalid");
2850
+ let value = line.slice(separator + 1).trim();
2851
+ if (value[0] === '"' || value[0] === "'")
2852
+ value = value.slice(1, -1);
2853
+ found[name] = { at, value };
2854
+ }
2855
+ return { parts, ...found };
2856
+ }
2857
+ function decodeServerConfig(source) {
2858
+ const { port, token } = settings(source);
2859
+ if (!port || !/^[1-9][0-9]{0,4}$/.test(port.value))
2860
+ throw new ContractError("invalid");
2861
+ if (!token)
2862
+ throw new ContractError("invalid");
2863
+ return {
2864
+ port: integer(Number(port.value), 1, 65535),
2865
+ token: accessToken(token.value)
2866
+ };
2867
+ }
2868
+
2869
+ class ServerConfigRefusal extends ContractError {
2870
+ path;
2871
+ constructor(path2) {
2872
+ super("corrupt-selected");
2873
+ this.path = path2;
2874
+ }
2875
+ }
2876
+ function decoded(snapshot) {
2877
+ return new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes);
2878
+ }
2879
+ function readServerConfig(path2) {
2880
+ 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);
2893
+ }
2894
+ }
2895
+ function ensureServerConfig(home) {
2896
+ 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);
2906
+ const lines = [];
2907
+ if (!found.port)
2908
+ lines.push(`${PORT_KEY}=${DEFAULT_PORT}`);
2909
+ if (!found.token)
2910
+ lines.push(`${TOKEN_KEY}=${randomBytes(32).toString("base64url")}`);
2911
+ if (lines.length) {
2912
+ const eol = source.includes(`\r
2913
+ `) ? `\r
2914
+ ` : `
2915
+ `;
2916
+ const ending = !source || /[\r\n]$/.test(source) ? "" : eol;
2917
+ 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("="))));
2920
+ }
2921
+ } catch {
2922
+ throw new ServerConfigRefusal(path2);
2923
+ }
2924
+ return { ...readServerConfig(path2), added };
2925
+ }
2926
+
2927
+ // src/service/autostart.ts
2928
+ var UNIT = "cadence.service";
2929
+ var MARKER = "# Generated by @abelxiaoxing/cadence";
2930
+ function wscript(env) {
2931
+ const root = env.SystemRoot;
2932
+ return win32.join(root && win32.isAbsolute(root) ? root : "C:\\Windows", "System32", "wscript.exe");
2933
+ }
2934
+ function hostSystem(root, owner) {
2935
+ const run = (command, args) => {
2936
+ const r = spawnSync2(command, args, {
2937
+ encoding: "utf8",
2938
+ stdio: ["ignore", "pipe", "ignore"],
2939
+ windowsHide: true
2940
+ });
2941
+ return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
2942
+ };
2943
+ return {
2944
+ platform: process.platform,
2945
+ env: process.env,
2946
+ home: homedir3(),
2947
+ node: process.execPath,
2948
+ root,
2949
+ unit: UNIT,
2950
+ uid: process.getuid?.(),
2951
+ systemctl: (args) => run("systemctl", ["--user", ...args]),
2952
+ linger: () => {
2953
+ const r = run("loginctl", [
2954
+ "show-user",
2955
+ String(process.getuid?.()),
2956
+ "--property=Linger",
2957
+ "--value"
2958
+ ]);
2959
+ return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
2960
+ },
2961
+ enableLinger: (interactive) => {
2962
+ const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
2963
+ "--system",
2964
+ "--allow-interactive-authorization=no",
2965
+ "--timeout=15",
2966
+ "call",
2967
+ "org.freedesktop.login1",
2968
+ "/org/freedesktop/login1",
2969
+ "org.freedesktop.login1.Manager",
2970
+ "SetUserLinger",
2971
+ "ubb",
2972
+ String(process.getuid?.()),
2973
+ "true",
2974
+ "false"
2975
+ ], { stdio: "ignore", timeout: 20000 });
2976
+ return !r.error && r.status === 0;
2977
+ },
2978
+ launchctl: (args) => run("launchctl", args),
2979
+ reg: (args) => run("reg", args),
2980
+ launch: (path2) => {
2981
+ const r = spawnSync2(wscript(process.env), [path2], {
2982
+ stdio: "ignore",
2983
+ windowsHide: true,
2984
+ timeout: 20000
2985
+ });
2986
+ return !r.error && r.status === 0;
2987
+ },
2988
+ ...owner
2989
+ };
2990
+ }
2991
+ function expandHome(value, home) {
2992
+ if (value === "~")
2993
+ return home;
2994
+ return value.startsWith("~/") ? join4(home, value.slice(2)) : value;
2995
+ }
2996
+ function agentDirectory(s) {
2997
+ const selected = s.env.PI_CODING_AGENT_DIR;
2998
+ return resolve3(selected ? expandHome(selected, s.home) : join4(s.home, ".pi", "agent"));
2999
+ }
3000
+ function piUserInstall(s) {
3001
+ try {
3002
+ const managed = join4(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
3003
+ return realpathSync2(managed) === realpathSync2(s.root);
3004
+ } catch {
3005
+ return false;
3006
+ }
3007
+ }
3008
+ function sdkPackages(root) {
3009
+ let start;
3010
+ try {
3011
+ start = realpathSync2(join4(root, "src"));
3012
+ } catch {
3013
+ return;
3014
+ }
3015
+ const found = [];
3016
+ for (const name of ["pi-coding-agent", "pi-ai"]) {
3017
+ let hit;
3018
+ for (let dir = start;!hit; dir = dirname4(dir)) {
3019
+ const candidate = join4(dir, "node_modules", "@earendil-works", name, "package.json");
3020
+ if (regularFile(candidate))
3021
+ hit = candidate;
3022
+ else if (dirname4(dir) === dir)
3023
+ return;
3024
+ }
3025
+ found.push(hit);
3026
+ }
3027
+ return found;
3028
+ }
3029
+ function unitPath(s) {
3030
+ const data = s.env.XDG_DATA_HOME;
3031
+ return join4(data && isAbsolute3(data) ? data : join4(s.home, ".local", "share"), "systemd", "user", s.unit);
3032
+ }
3033
+ function servicePath(value) {
3034
+ const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute3(entry) && !entry.endsWith(`${sep3}node_modules${sep3}.bin`) && !entry.includes(`${sep3}node-gyp-bin`));
3035
+ return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
3036
+ }
3037
+ function serviceEnvironment(s) {
3038
+ const out = [["PATH", servicePath(s.env.PATH)]];
3039
+ for (const key of ["LANG", "LC_ALL"]) {
3040
+ const value = s.env[key];
3041
+ if (value)
3042
+ out.push([key, value]);
3043
+ }
3044
+ const state = s.env.XDG_STATE_HOME;
3045
+ if (state && isAbsolute3(state))
3046
+ out.push(["XDG_STATE_HOME", state]);
3047
+ if (s.env.PI_CODING_AGENT_DIR)
3048
+ out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
3049
+ return out;
3050
+ }
3051
+ function checked(value) {
3052
+ if (/[\u0000-\u001f\u007f]/.test(value))
3053
+ throw new ContractError("invalid");
3054
+ return value;
3055
+ }
3056
+ var specifiers = (value) => checked(value).replaceAll("%", "%%");
3057
+ var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
3058
+ var argument = (value) => quoted(value).replaceAll("$", "$$$$");
3059
+ function buildUnit(u) {
3060
+ const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
3104
3061
  return [
3105
- st.dev,
3106
- st.ino,
3107
- st.size,
3108
- st.mode,
3109
- st.uid,
3110
- st.nlink,
3111
- st.ctimeMs,
3112
- st.mtimeMs
3113
- ].join(":");
3062
+ `${MARKER}; \`cadence server install\` and \`restart\` rewrite this file.`,
3063
+ "# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
3064
+ "[Unit]",
3065
+ "Description=Cadence shared task service and WebUI",
3066
+ "Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
3067
+ `ConditionPathExists=${specifiers(u.cli)}`,
3068
+ `ConditionPathExists=${specifiers(join4(u.agentDir, "auth.json"))}`,
3069
+ ...u.sdk.map((path2) => `ConditionPathExists=${specifiers(path2)}`),
3070
+ "StartLimitIntervalSec=300",
3071
+ "StartLimitBurst=3",
3072
+ "",
3073
+ "[Service]",
3074
+ "Type=simple",
3075
+ `WorkingDirectory=${specifiers(u.workspace)}`,
3076
+ ...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
3077
+ `ExecStart=${command(["start", "--foreground", "--workspace", u.workspace, "--server-config", u.serverConfig])}`,
3078
+ `ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
3079
+ "KillMode=mixed",
3080
+ "TimeoutStopSec=90",
3081
+ "Restart=on-failure",
3082
+ "RestartSec=10",
3083
+ "",
3084
+ "[Install]",
3085
+ "WantedBy=default.target",
3086
+ ""
3087
+ ].join(`
3088
+ `);
3114
3089
  }
3115
- function json(snapshot, maximum = METADATA_BYTES) {
3090
+ function readGenerated(path2, marker) {
3116
3091
  try {
3117
- return parseWireJson(new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes), maximum);
3092
+ if (!lstatSync3(path2).isFile())
3093
+ return null;
3094
+ const bytes = readFileSync(path2);
3095
+ const text2 = bytes[0] === 255 && bytes[1] === 254 ? bytes.subarray(2).toString("utf16le") : bytes.toString("utf8");
3096
+ return text2.startsWith(marker) ? text2 : null;
3097
+ } catch (error) {
3098
+ if (error.code === "ENOENT")
3099
+ return;
3100
+ throw error;
3101
+ }
3102
+ }
3103
+ function unitWorkspace(text2) {
3104
+ return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
3105
+ }
3106
+ function directory(path2) {
3107
+ try {
3108
+ return isAbsolute3(path2) && statSync(path2).isDirectory();
3118
3109
  } catch {
3119
- return refused();
3110
+ return false;
3120
3111
  }
3121
3112
  }
3122
- function encoded(value) {
3123
- return `${JSON.stringify(value)}
3124
- `;
3113
+ function serviceStateHome(s) {
3114
+ const state = s.platform === "win32" ? s.env.LOCALAPPDATA : s.env.XDG_STATE_HOME;
3115
+ return state && isAbsolute3(state) ? state : join4(s.home, ...s.platform === "win32" ? ["AppData", "Local"] : [".local", "state"]);
3116
+ }
3117
+ function selectWorkspace(s, requested, kept) {
3118
+ let chosen;
3119
+ if (requested) {
3120
+ chosen = expandHome(requested, s.home);
3121
+ if (!directory(chosen))
3122
+ throw new ContractError("invalid");
3123
+ chosen = resolve3(chosen);
3124
+ } else if (kept && directory(kept))
3125
+ chosen = kept;
3126
+ const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace: join4(s.home, LANDING_WORKSPACE), redirected: false };
3127
+ if (landing.workspace !== chosen)
3128
+ mkdirSync4(landing.workspace, { recursive: true });
3129
+ return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
3130
+ }
3131
+ function write(path2, content) {
3132
+ mkdirSync4(dirname4(path2), { recursive: true });
3133
+ const temporary = `${path2}.${process.pid}.tmp`;
3134
+ try {
3135
+ writeFileSync3(temporary, content, { mode: 420, flag: "wx" });
3136
+ renameSync3(temporary, path2);
3137
+ } finally {
3138
+ rmSync(temporary, { force: true });
3139
+ }
3140
+ }
3141
+ function regularFile(path2) {
3142
+ try {
3143
+ return statSync(path2).isFile();
3144
+ } catch {
3145
+ return false;
3146
+ }
3147
+ }
3148
+ async function settle(s, workspace) {
3149
+ if (!workspace)
3150
+ return;
3151
+ const status = await s.stopOwner(workspace);
3152
+ if (status === "stopped")
3153
+ return;
3154
+ return {
3155
+ status: status === "quarantined" || status === "unavailable" ? status : "unconfirmed",
3156
+ workspace
3157
+ };
3158
+ }
3159
+ function bootStart(s, attempt) {
3160
+ const current = s.linger();
3161
+ if (current === undefined)
3162
+ return {};
3163
+ if (current)
3164
+ return { linger: "on" };
3165
+ if (!attempt)
3166
+ return { linger: "off" };
3167
+ return {
3168
+ linger: s.enableLinger(false) || s.enableLinger(true) ? "enabled" : "refused"
3169
+ };
3170
+ }
3171
+ function configureAutostart(s, mode, requested, linger = false) {
3172
+ if (s.platform !== "linux")
3173
+ return { status: "unsupported" };
3174
+ if (s.systemctl(["show-environment"]).status !== 0)
3175
+ return { status: "unavailable" };
3176
+ const path2 = unitPath(s);
3177
+ const previous = readGenerated(path2, MARKER);
3178
+ if (previous === null)
3179
+ return { status: "foreign", unit: path2 };
3180
+ if (mode === "restart" && previous === undefined)
3181
+ return { status: "absent" };
3182
+ const sdk = sdkPackages(s.root);
3183
+ if (!sdk)
3184
+ return { status: "missing-pi-sdk" };
3185
+ const { workspace, redirectedFrom } = selectWorkspace(s, requested, unitWorkspace(previous));
3186
+ const agentDir = agentDirectory(s);
3187
+ const content = buildUnit({
3188
+ node: s.node,
3189
+ cli: join4(s.root, "src", "service-cli.mjs"),
3190
+ workspace,
3191
+ serverConfig: serverConfigPath(s.home),
3192
+ agentDir,
3193
+ sdk,
3194
+ environment: serviceEnvironment(s)
3195
+ });
3196
+ const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
3197
+ const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
3198
+ if (active)
3199
+ s.systemctl(["stop", s.unit]);
3200
+ write(path2, content);
3201
+ s.systemctl(["daemon-reload"]);
3202
+ const result = {
3203
+ unit: path2,
3204
+ workspace,
3205
+ ...redirectedFrom ? { redirectedFrom } : {}
3206
+ };
3207
+ if (enabled.startsWith("masked"))
3208
+ return { status: "masked", ...result };
3209
+ if (mode === "install" && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
3210
+ return { status: "failed", ...result };
3211
+ const boot = () => mode === "install" ? bootStart(s, linger) : {};
3212
+ if (!regularFile(join4(agentDir, "auth.json")))
3213
+ return { status: "waiting-for-pi-login", ...result, ...boot() };
3214
+ s.systemctl(["reset-failed", s.unit]);
3215
+ const started = s.systemctl(["start", s.unit]).status === 0;
3216
+ return { status: started ? "started" : "failed", ...result, ...boot() };
3217
+ }
3218
+ function stopAutostart(s) {
3219
+ if (s.platform !== "linux")
3220
+ return { status: "unsupported" };
3221
+ const path2 = unitPath(s);
3222
+ const previous = readGenerated(path2, MARKER);
3223
+ if (previous === null)
3224
+ return { status: "foreign", unit: path2 };
3225
+ if (previous === undefined)
3226
+ return { status: "absent" };
3227
+ if (s.systemctl(["show-environment"]).status !== 0)
3228
+ return { status: "unavailable", unit: path2 };
3229
+ return {
3230
+ status: s.systemctl(["stop", s.unit]).status === 0 ? "stopped" : "failed",
3231
+ unit: path2,
3232
+ workspace: unitWorkspace(previous)
3233
+ };
3234
+ }
3235
+ function removeAutostart(s) {
3236
+ if (s.platform !== "linux")
3237
+ return { status: "unsupported" };
3238
+ const path2 = unitPath(s);
3239
+ const previous = readGenerated(path2, MARKER);
3240
+ if (previous === null)
3241
+ return { status: "foreign", unit: path2 };
3242
+ if (previous === undefined)
3243
+ return { status: "absent" };
3244
+ if (s.systemctl(["show-environment"]).status !== 0)
3245
+ return { status: "unavailable", unit: path2 };
3246
+ if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
3247
+ return { status: "failed", unit: path2 };
3248
+ rmSync(path2);
3249
+ s.systemctl(["daemon-reload"]);
3250
+ s.systemctl(["reset-failed", s.unit]);
3251
+ return { status: "removed", unit: path2 };
3252
+ }
3253
+ function autostartStatus(s) {
3254
+ if (s.platform !== "linux")
3255
+ return { status: "unsupported" };
3256
+ const path2 = unitPath(s);
3257
+ const previous = readGenerated(path2, MARKER);
3258
+ if (previous === null)
3259
+ return { status: "foreign", unit: path2 };
3260
+ if (previous === undefined)
3261
+ return { status: "absent" };
3262
+ return {
3263
+ status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
3264
+ enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
3265
+ unit: path2,
3266
+ workspace: unitWorkspace(previous),
3267
+ ...bootStart(s, false)
3268
+ };
3125
3269
  }
3126
3270
 
3127
3271
  // src/service/coordination.ts
3272
+ import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
3273
+ import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
3274
+ import { hostname as hostname2 } from "node:os";
3275
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join5, relative as relative3, sep as sep4 } from "node:path";
3128
3276
  var registrationKeys = [
3129
3277
  "schema",
3130
3278
  "root",
@@ -3250,17 +3398,17 @@ class ServiceCoordination {
3250
3398
  stateHome;
3251
3399
  constructor(workspace, stateHome) {
3252
3400
  this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
3253
- const namespace = dirname4(this.store.privateRoot);
3401
+ const namespace = dirname5(this.store.privateRoot);
3254
3402
  this.files = new PrivateFiles(namespace);
3255
- this.stateHome = dirname4(namespace);
3256
- this.directory = join4(this.store.privateRoot, "service-v1");
3257
- this.registryDirectory = join4(namespace, "service-registry-v1");
3403
+ this.stateHome = dirname5(namespace);
3404
+ this.directory = join5(this.store.privateRoot, "service-v1");
3405
+ this.registryDirectory = join5(namespace, "service-registry-v1");
3258
3406
  }
3259
3407
  path(name) {
3260
- return join4(this.directory, name);
3408
+ return join5(this.directory, name);
3261
3409
  }
3262
3410
  registryPath(name) {
3263
- return join4(this.registryDirectory, name);
3411
+ return join5(this.registryDirectory, name);
3264
3412
  }
3265
3413
  registry() {
3266
3414
  if (!this.files.directory(this.registryDirectory))
@@ -3308,7 +3456,7 @@ class ServiceCoordination {
3308
3456
  "host-config.json",
3309
3457
  "history-v2"
3310
3458
  ]);
3311
- this.files.directory(join4(this.directory, "history-v2"), false, walks);
3459
+ this.files.directory(join5(this.directory, "history-v2"), false, walks);
3312
3460
  }
3313
3461
  selected() {
3314
3462
  const walks = new DirectoryWalks;
@@ -3468,7 +3616,7 @@ class ServiceCoordination {
3468
3616
  throw error;
3469
3617
  }
3470
3618
  }
3471
- acquire(input, resolvedEndpoint = input.endpoint, initializeNew) {
3619
+ acquire(input, resolvedEndpoint = input.endpoint, initializeNew, reselectExisting) {
3472
3620
  const config = decodeOwnerConfig(input);
3473
3621
  const bound = endpoint(resolvedEndpoint);
3474
3622
  if (!endpointMatches(config.endpoint, bound))
@@ -3483,7 +3631,8 @@ class ServiceCoordination {
3483
3631
  throw new ContractError("quarantine");
3484
3632
  if (this.store.inspectLock())
3485
3633
  throw new ContractError("conflict");
3486
- if (old && old.config.identity !== hash(encoded(config)))
3634
+ const reselect = !!old && old.config.identity !== hash(encoded(config));
3635
+ if (reselect && (!reselectExisting || !old?.hostConfig))
3487
3636
  throw new ContractError("conflict");
3488
3637
  const row = {
3489
3638
  root: this.store.root,
@@ -3500,7 +3649,17 @@ class ServiceCoordination {
3500
3649
  ...registry.value,
3501
3650
  entries: [...registry.value.entries, row]
3502
3651
  }), REGISTRY_BYTES);
3503
- this.initialize(config);
3652
+ if (old?.hostConfig && reselect && reselectExisting) {
3653
+ reselectExisting(old.hostConfig);
3654
+ const saved = this.files.replace(this.path("config.json"), old.config, encoded(config));
3655
+ this.files.replace(this.path("format.json"), old.manifest, encoded({
3656
+ schema: "cadence-service-format/1",
3657
+ root: this.store.root,
3658
+ stateHome: this.stateHome,
3659
+ configIdentity: saved.identity
3660
+ }));
3661
+ } else
3662
+ this.initialize(config);
3504
3663
  if (!old)
3505
3664
  initializeNew?.();
3506
3665
  const checkout = this.store.acquire();
@@ -3517,7 +3676,7 @@ class ServiceCoordination {
3517
3676
  const capability = this.files.create(this.path("local-capability.json"), encoded({
3518
3677
  schema: "cadence-service-local-capability/1",
3519
3678
  epoch: row.epoch,
3520
- capability: randomBytes(32).toString("base64url")
3679
+ capability: randomBytes2(32).toString("base64url")
3521
3680
  }));
3522
3681
  const value = {
3523
3682
  schema: "cadence-service-registration/1",
@@ -3730,12 +3889,12 @@ class OwnerLease {
3730
3889
 
3731
3890
  // src/service/launchd.ts
3732
3891
  import { rmSync as rmSync2 } from "node:fs";
3733
- import { join as join5 } from "node:path";
3892
+ import { join as join6 } from "node:path";
3734
3893
  var LABEL = "com.abelxiaoxing.cadence";
3735
3894
  var MARKER2 = `<?xml version="1.0" encoding="UTF-8"?>
3736
3895
  <!-- Generated by @abelxiaoxing/cadence`;
3737
3896
  function plistPath(s) {
3738
- return join5(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
3897
+ return join6(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
3739
3898
  }
3740
3899
  var xml = (value) => checked(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3741
3900
  function buildPlist(u) {
@@ -3750,7 +3909,16 @@ function buildPlist(u) {
3750
3909
  ` <string>${LABEL}</string>`,
3751
3910
  "\t<key>ProgramArguments</key>",
3752
3911
  "\t<array>",
3753
- ...[u.node, u.cli, "start", "--foreground", "--workspace", u.workspace].map(string),
3912
+ ...[
3913
+ u.node,
3914
+ u.cli,
3915
+ "start",
3916
+ "--foreground",
3917
+ "--workspace",
3918
+ u.workspace,
3919
+ "--server-config",
3920
+ u.serverConfig
3921
+ ].map(string),
3754
3922
  "\t</array>",
3755
3923
  "\t<key>WorkingDirectory</key>",
3756
3924
  ` <string>${xml(u.workspace)}</string>`,
@@ -3806,10 +3974,11 @@ async function configureLaunchAgent(s, mode, requested) {
3806
3974
  s.launchctl(["bootout", target(s)]);
3807
3975
  write(path2, buildPlist({
3808
3976
  node: s.node,
3809
- cli: join5(s.root, "src", "service-cli.mjs"),
3977
+ cli: join6(s.root, "src", "service-cli.mjs"),
3810
3978
  workspace,
3979
+ serverConfig: serverConfigPath(s.home),
3811
3980
  environment: serviceEnvironment(s),
3812
- log: join5(s.home, "Library", "Logs", "cadence.log")
3981
+ log: join6(s.home, "Library", "Logs", "cadence.log")
3813
3982
  }));
3814
3983
  const result = {
3815
3984
  unit: path2,
@@ -3820,7 +3989,7 @@ async function configureLaunchAgent(s, mode, requested) {
3820
3989
  s.launchctl(["enable", target(s)]);
3821
3990
  else if (disabled(s))
3822
3991
  return { status: "disabled", ...result };
3823
- if (!regularFile(join5(agentDirectory(s), "auth.json")))
3992
+ if (!regularFile(join6(agentDirectory(s), "auth.json")))
3824
3993
  return { status: "waiting-for-pi-login", ...result };
3825
3994
  const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
3826
3995
  return { status: started ? "started" : "failed", ...result };
@@ -3869,14 +4038,14 @@ function launchAgentStatus(s) {
3869
4038
 
3870
4039
  // src/service/login-run.ts
3871
4040
  import { rmSync as rmSync3 } from "node:fs";
3872
- import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
4041
+ import { isAbsolute as isAbsolute5, join as join7 } from "node:path";
3873
4042
  var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
3874
4043
  var APPROVED_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run";
3875
4044
  var RUN_VALUE = "Cadence";
3876
4045
  var MARKER3 = "' Generated by @abelxiaoxing/cadence";
3877
4046
  function launcherPath(s) {
3878
4047
  const local = s.env.LOCALAPPDATA;
3879
- return join6(local && isAbsolute5(local) ? local : join6(s.home, "AppData", "Local"), "cadence-server", "cadence.vbs");
4048
+ return join7(local && isAbsolute5(local) ? local : join7(s.home, "AppData", "Local"), "cadence-server", "cadence.vbs");
3880
4049
  }
3881
4050
  var literal2 = (value) => `"${checked(value).replaceAll('"', '""')}"`;
3882
4051
  function argument2(value) {
@@ -3885,7 +4054,7 @@ function argument2(value) {
3885
4054
  return `"${value}"`;
3886
4055
  }
3887
4056
  function buildLauncher(u) {
3888
- const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)}`;
4057
+ const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)} --server-config ${argument2(u.serverConfig)}`;
3889
4058
  return [
3890
4059
  `${MARKER3}; \`cadence server install\` and \`restart\` rewrite this file.`,
3891
4060
  `' Started at login by ${RUN_KEY}\\${RUN_VALUE}; window style 0 hides the console.`,
@@ -3929,8 +4098,9 @@ async function configureLoginRun(s, mode, requested) {
3929
4098
  const environment = s.env.PI_CODING_AGENT_DIR ? [["PI_CODING_AGENT_DIR", agentDirectory(s)]] : [];
3930
4099
  write(path2, utf16(buildLauncher({
3931
4100
  node: s.node,
3932
- cli: join6(s.root, "src", "service-cli.mjs"),
4101
+ cli: join7(s.root, "src", "service-cli.mjs"),
3933
4102
  workspace,
4103
+ serverConfig: serverConfigPath(s.home),
3934
4104
  environment
3935
4105
  })));
3936
4106
  const result = {
@@ -3944,7 +4114,7 @@ async function configureLoginRun(s, mode, requested) {
3944
4114
  return { status: "failed", ...result };
3945
4115
  s.reg(["delete", APPROVED_KEY, "/v", RUN_VALUE, "/f"]);
3946
4116
  }
3947
- if (!regularFile(join6(agentDirectory(s), "auth.json")))
4117
+ if (!regularFile(join7(agentDirectory(s), "auth.json")))
3948
4118
  return { status: "waiting-for-pi-login", ...result };
3949
4119
  return { status: s.launch(path2) ? "started" : "failed", ...result };
3950
4120
  }
@@ -3998,7 +4168,7 @@ import { isatty } from "node:tty";
3998
4168
  // src/history/recovery.ts
3999
4169
  import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
4000
4170
  import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
4001
- import { join as join8, relative as relative4 } from "node:path";
4171
+ import { join as join9, relative as relative4 } from "node:path";
4002
4172
 
4003
4173
  // src/service/recovery.ts
4004
4174
  async function inspectRecovery(coordination, history) {
@@ -4046,7 +4216,7 @@ async function resolveRecovery(coordination, confirmation, history) {
4046
4216
  }
4047
4217
 
4048
4218
  // src/history/files.ts
4049
- import { createHash as createHash4, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "node:crypto";
4219
+ import { createHash as createHash4, randomBytes as randomBytes3, randomUUID as randomUUID4 } from "node:crypto";
4050
4220
  import {
4051
4221
  closeSync as closeSync5,
4052
4222
  constants as constants5,
@@ -4058,7 +4228,7 @@ import {
4058
4228
  statfsSync
4059
4229
  } from "node:fs";
4060
4230
  import { open } from "node:fs/promises";
4061
- import { dirname as dirname6, join as join7 } from "node:path";
4231
+ import { dirname as dirname7, join as join8 } from "node:path";
4062
4232
 
4063
4233
  // src/history/admission.ts
4064
4234
  function decodeRunAdmission(value) {
@@ -4086,13 +4256,13 @@ import {
4086
4256
  openSync as openSync4,
4087
4257
  readSync as readSync3
4088
4258
  } from "node:fs";
4089
- import { dirname as dirname5 } from "node:path";
4259
+ import { dirname as dirname6 } from "node:path";
4090
4260
  function bad() {
4091
4261
  throw new ContractError("corrupt-selected");
4092
4262
  }
4093
4263
  var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
4094
4264
  function selected(files, path2, read, walks) {
4095
- if (!files.files.directory(dirname5(path2), false, walks))
4265
+ if (!files.files.directory(dirname6(path2), false, walks))
4096
4266
  bad();
4097
4267
  const before = lstatSync4(path2);
4098
4268
  if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
@@ -4589,14 +4759,14 @@ class HistoryFiles {
4589
4759
  this.coordination = coordination;
4590
4760
  this.format = format;
4591
4761
  this.formatSnapshot = formatSnapshot;
4592
- this.directory = join7(coordination.directory, "history-v2");
4762
+ this.directory = join8(coordination.directory, "history-v2");
4593
4763
  this.files = coordination.files;
4594
4764
  }
4595
4765
  static open(coordination) {
4596
- const dir = join7(coordination.directory, "history-v2");
4766
+ const dir = join8(coordination.directory, "history-v2");
4597
4767
  if (!coordination.files.directory(dir))
4598
4768
  return;
4599
- const file = coordination.files.read(join7(dir, "format.json"));
4769
+ const file = coordination.files.read(join8(dir, "format.json"));
4600
4770
  if (!file)
4601
4771
  corrupt();
4602
4772
  const format = decodeFormat(json(file));
@@ -4612,7 +4782,7 @@ class HistoryFiles {
4612
4782
  existing.assertWritable();
4613
4783
  return existing;
4614
4784
  }
4615
- const dir = join7(c.directory, "history-v2");
4785
+ const dir = join8(c.directory, "history-v2");
4616
4786
  const format = {
4617
4787
  schema: FORMAT,
4618
4788
  storeId: randomUUID4(),
@@ -4622,19 +4792,19 @@ class HistoryFiles {
4622
4792
  c.files.directory(dir, true);
4623
4793
  for (const name of ["commands", "ledger", "runs", "indexes"]) {
4624
4794
  lease.assert();
4625
- c.files.directory(join7(dir, name), true);
4795
+ c.files.directory(join8(dir, name), true);
4626
4796
  }
4627
4797
  lease.assert();
4628
- const keyPath = join7(dir, "read-proof-key.json");
4798
+ const keyPath = join8(dir, "read-proof-key.json");
4629
4799
  c.files.create(keyPath, encoded({
4630
4800
  schema: "cadence-history-read-key/1",
4631
4801
  storeId: format.storeId,
4632
- key: randomBytes2(32).toString("base64url")
4802
+ key: randomBytes3(32).toString("base64url")
4633
4803
  }));
4634
4804
  lease.assert();
4635
- const snapshot = c.files.create(join7(dir, "format.json"), encoded(format));
4805
+ const snapshot = c.files.create(join8(dir, "format.json"), encoded(format));
4636
4806
  const result = new HistoryFiles(c, format, snapshot);
4637
- result.syncParents(join7(dir, "format.json"));
4807
+ result.syncParents(join8(dir, "format.json"));
4638
4808
  result.assertWritable();
4639
4809
  return result;
4640
4810
  }
@@ -4669,7 +4839,7 @@ class HistoryFiles {
4669
4839
  throw new ContractError("quarantine");
4670
4840
  }
4671
4841
  assertFormat() {
4672
- this.files.assert(join7(this.directory, "format.json"), this.formatSnapshot);
4842
+ this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot);
4673
4843
  }
4674
4844
  readProofMaterial(budget) {
4675
4845
  if (this.format.schema !== FORMAT)
@@ -4678,7 +4848,7 @@ class HistoryFiles {
4678
4848
  this.assertReadFormat(budget);
4679
4849
  else
4680
4850
  this.assertFormat();
4681
- const selected2 = this.readBudgeted(join7(this.directory, "read-proof-key.json"), 512, budget);
4851
+ const selected2 = this.readBudgeted(join8(this.directory, "read-proof-key.json"), 512, budget);
4682
4852
  if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
4683
4853
  corrupt();
4684
4854
  try {
@@ -4706,17 +4876,17 @@ class HistoryFiles {
4706
4876
  return;
4707
4877
  }
4708
4878
  this.assertFormat();
4709
- assertIncarnation(this, join7(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
4879
+ assertIncarnation(this, join8(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
4710
4880
  }
4711
4881
  segmentPath(segment) {
4712
- return join7(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
4882
+ return join8(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
4713
4883
  }
4714
4884
  artifactPath(ref) {
4715
- return join7(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
4885
+ return join8(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
4716
4886
  }
4717
4887
  admissionPath(runId) {
4718
4888
  decodeId(runId);
4719
- return join7(this.directory, "runs", runId, "admission.json");
4889
+ return join8(this.directory, "runs", runId, "admission.json");
4720
4890
  }
4721
4891
  writeAdmission(lease, input) {
4722
4892
  lease.assert();
@@ -4742,7 +4912,7 @@ class HistoryFiles {
4742
4912
  readCommand(commandId, budget) {
4743
4913
  decodeId(commandId);
4744
4914
  this.assertReadFormat(budget);
4745
- return this.readBudgeted(join7(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
4915
+ return this.readBudgeted(join8(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
4746
4916
  }
4747
4917
  readAdmission(runId, identity, budget) {
4748
4918
  return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
@@ -4763,7 +4933,7 @@ class HistoryFiles {
4763
4933
  syncParents(file) {
4764
4934
  if (process.platform === "win32")
4765
4935
  return;
4766
- for (let path2 = dirname6(file);; path2 = dirname6(path2)) {
4936
+ for (let path2 = dirname7(file);; path2 = dirname7(path2)) {
4767
4937
  if (!this.files.directory(path2))
4768
4938
  corrupt();
4769
4939
  const fd = openSync5(path2, constants5.O_RDONLY | constants5.O_DIRECTORY);
@@ -4774,13 +4944,13 @@ class HistoryFiles {
4774
4944
  }
4775
4945
  if (path2 === this.coordination.directory)
4776
4946
  break;
4777
- if (path2 === dirname6(path2))
4947
+ if (path2 === dirname7(path2))
4778
4948
  corrupt();
4779
4949
  }
4780
4950
  }
4781
4951
  assertReadFormat(budget) {
4782
4952
  budget.reserve(this.formatSnapshot.bytes.length);
4783
- this.files.assert(join7(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
4953
+ this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
4784
4954
  }
4785
4955
  assertPublished(segment, end, seq) {
4786
4956
  const fence = publicationFences.get(this.directory);
@@ -4818,7 +4988,7 @@ class HistoryFiles {
4818
4988
  const dir = opendirSync2(path2);
4819
4989
  try {
4820
4990
  for (let e = dir.readSync();e; e = dir.readSync()) {
4821
- const p = join7(path2, e.name), st = lstatSync5(p);
4991
+ const p = join8(path2, e.name), st = lstatSync5(p);
4822
4992
  if (st.isDirectory())
4823
4993
  bytes += walk(p, depth + 1);
4824
4994
  else {
@@ -4853,7 +5023,7 @@ class HistoryFiles {
4853
5023
  state.bytes = 0;
4854
5024
  state.identity = null;
4855
5025
  let count = 0;
4856
- const ledger = join7(this.directory, "ledger");
5026
+ const ledger = join8(this.directory, "ledger");
4857
5027
  if (!this.files.directory(ledger))
4858
5028
  corrupt();
4859
5029
  const dir = opendirSync2(ledger);
@@ -4901,9 +5071,9 @@ class HistoryFiles {
4901
5071
  const line = file.bytes.subarray(at, end + 1);
4902
5072
  if (line.length > RECORD_BYTES)
4903
5073
  corrupt();
4904
- let decoded;
5074
+ let decoded2;
4905
5075
  try {
4906
- decoded = decodeJournalText(new TextDecoder("utf-8", { fatal: true }).decode(line));
5076
+ decoded2 = decodeJournalText(new TextDecoder("utf-8", { fatal: true }).decode(line));
4907
5077
  } catch {
4908
5078
  corrupt();
4909
5079
  }
@@ -4911,29 +5081,29 @@ class HistoryFiles {
4911
5081
  state.records++;
4912
5082
  if (state.records > LIMITS.segmentRecords)
4913
5083
  corrupt();
4914
- if (decoded.kind === "record") {
5084
+ if (decoded2.kind === "record") {
4915
5085
  let base = state.committedSeq;
4916
- if (decoded.value.kind === "recovery") {
4917
- if (pending.length || at !== line.length || n !== decoded.value.previous.segment + 1)
5086
+ if (decoded2.value.kind === "recovery") {
5087
+ if (pending.length || at !== line.length || n !== decoded2.value.previous.segment + 1)
4918
5088
  corrupt();
4919
- if (decoded.value.previous.segment !== preceding.segment || decoded.value.previous.bytes !== preceding.bytes || decoded.value.previous.sha256 !== (preceding.identity ?? hash("")) || decoded.value.previous.committedSeq !== preceding.committedSeq || decoded.value.previous.highSeq !== (preceding.highSeq ?? preceding.committedSeq) || decoded.value.previous.committedBytes !== (preceding.committedBytes ?? 0))
5089
+ if (decoded2.value.previous.segment !== preceding.segment || decoded2.value.previous.bytes !== preceding.bytes || decoded2.value.previous.sha256 !== (preceding.identity ?? hash("")) || decoded2.value.previous.committedSeq !== preceding.committedSeq || decoded2.value.previous.highSeq !== (preceding.highSeq ?? preceding.committedSeq) || decoded2.value.previous.committedBytes !== (preceding.committedBytes ?? 0))
4920
5090
  corrupt();
4921
- base = decoded.value.previous.highSeq;
5091
+ base = decoded2.value.previous.highSeq;
4922
5092
  } else if (preceding.tail && state.committedBytes === 0)
4923
5093
  corrupt();
4924
5094
  const next = BigInt(base) + BigInt(pending.length) + 1n;
4925
- if (decoded.seq !== String(next))
5095
+ if (decoded2.seq !== String(next))
4926
5096
  corrupt();
4927
- state.highSeq = decoded.seq;
5097
+ state.highSeq = decoded2.seq;
4928
5098
  pendingBytes += line.length;
4929
5099
  if (pendingBytes > 2 * RECORD_BYTES)
4930
5100
  corrupt();
4931
- pending.push(decoded);
5101
+ pending.push(decoded2);
4932
5102
  checksum.update(line);
4933
5103
  continue;
4934
5104
  }
4935
5105
  const artifacts = batchArtifacts(pending);
4936
- if (!pending.length || !validBatchSequence(pending, decoded.previous) || decoded.previous !== state.committedSeq || decoded.first !== pending[0].seq || decoded.last !== pending.at(-1)?.seq || decoded.count !== pending.length || decoded.sha256 !== checksum.digest("hex") || canonicalJson(decoded.artifacts, RECORD_BYTES) !== canonicalJson(artifacts, RECORD_BYTES))
5106
+ if (!pending.length || !validBatchSequence(pending, decoded2.previous) || decoded2.previous !== state.committedSeq || decoded2.first !== pending[0].seq || decoded2.last !== pending.at(-1)?.seq || decoded2.count !== pending.length || decoded2.sha256 !== checksum.digest("hex") || canonicalJson(decoded2.artifacts, RECORD_BYTES) !== canonicalJson(artifacts, RECORD_BYTES))
4937
5107
  corrupt();
4938
5108
  if (verifyArtifacts)
4939
5109
  for (const artifact of artifacts)
@@ -4946,12 +5116,12 @@ class HistoryFiles {
4946
5116
  if (e.kind === "owner" && e.value.kind === "fact" && e.value.fact.kind === "parent-admitted")
4947
5117
  this.readAdmission(e.value.fact.runId, e.value.fact.identity, budget);
4948
5118
  }
4949
- state.committedSeq = decoded.last;
5119
+ state.committedSeq = decoded2.last;
4950
5120
  state.committedBytes = at;
4951
- state.navigationRoot = decoded.navigationRoot;
5121
+ state.navigationRoot = decoded2.navigationRoot;
4952
5122
  yield {
4953
5123
  rows: pending,
4954
- marker: decoded,
5124
+ marker: decoded2,
4955
5125
  segment: n,
4956
5126
  identity: file.identity,
4957
5127
  segmentBytes: file.bytes.length
@@ -5170,7 +5340,7 @@ class HistoryFiles {
5170
5340
  checkpoint(lease, state) {
5171
5341
  lease.assert();
5172
5342
  this.assertWritable();
5173
- const path2 = join7(this.directory, "checkpoint.json"), old = this.files.read(path2);
5343
+ const path2 = join8(this.directory, "checkpoint.json"), old = this.files.read(path2);
5174
5344
  const body = encoded({
5175
5345
  schema: "cadence-history-checkpoint/2",
5176
5346
  storeId: this.format.storeId,
@@ -5597,7 +5767,7 @@ function digestHistory(files, exclude) {
5597
5767
  dir.closeSync();
5598
5768
  }
5599
5769
  for (const name of names2.sort()) {
5600
- const selected2 = join8(path2, name);
5770
+ const selected2 = join9(path2, name);
5601
5771
  if (selected2 === exclude)
5602
5772
  continue;
5603
5773
  const rel = relative4(files.directory, selected2);
@@ -5908,7 +6078,7 @@ async function localRecovery(c, operation) {
5908
6078
 
5909
6079
  // src/service/runtime.ts
5910
6080
  import { homedir as homedir4 } from "node:os";
5911
- import { dirname as dirname7, isAbsolute as isAbsolute6, join as join9, resolve as resolve4 } from "node:path";
6081
+ import { dirname as dirname8, isAbsolute as isAbsolute6, join as join10, resolve as resolve4 } from "node:path";
5912
6082
 
5913
6083
  // src/service/config.ts
5914
6084
  import { isIP } from "node:net";
@@ -5978,10 +6148,11 @@ function decodeHostConfig(value) {
5978
6148
  const tokenSource = oneOf(o.tokenSource, [
5979
6149
  "generated",
5980
6150
  "environment",
5981
- "file"
6151
+ "file",
6152
+ "config"
5982
6153
  ]);
5983
6154
  const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
5984
- if (o.tokenSource === "file" !== (tokenFile !== null))
6155
+ if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
5985
6156
  throw new ContractError("invalid");
5986
6157
  return {
5987
6158
  schema: literal(o.schema, "cadence-service-host/1"),
@@ -6042,13 +6213,13 @@ function selectStart(options, ambient) {
6042
6213
  throw new ContractError("invalid");
6043
6214
  let fromFile;
6044
6215
  if (options.config) {
6045
- const path2 = resolve4(options.config), f = new PrivateFiles(dirname7(path2));
6216
+ const path2 = resolve4(options.config), f = new PrivateFiles(dirname8(path2));
6046
6217
  const bytes = f.read(path2);
6047
6218
  if (!bytes)
6048
6219
  throw new ContractError("corrupt-selected");
6049
6220
  fromFile = decodeHostConfig(json(bytes));
6050
6221
  }
6051
- const selected2 = old ?? fromFile ?? decodeHostConfig({
6222
+ const base = old ?? fromFile ?? decodeHostConfig({
6052
6223
  schema: "cadence-service-host/1",
6053
6224
  cwd: workspace.cwd,
6054
6225
  http: {
@@ -6057,10 +6228,23 @@ function selectStart(options, ambient) {
6057
6228
  port: options.port ?? 17423,
6058
6229
  proxy: null
6059
6230
  },
6060
- agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join9(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
6231
+ agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join10(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
6061
6232
  tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
6062
6233
  tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
6063
6234
  });
6235
+ const server = options.serverConfig !== undefined ? resolve4(options.serverConfig) : old?.tokenSource === "config" ? old.tokenFile : null;
6236
+ let selected2 = base;
6237
+ if (server) {
6238
+ if (options.tokenFile !== undefined)
6239
+ throw new ContractError("conflict");
6240
+ const { port } = readServerConfig(server).config;
6241
+ selected2 = decodeHostConfig({
6242
+ ...base,
6243
+ http: { ...base.http, port },
6244
+ tokenSource: "config",
6245
+ tokenFile: server
6246
+ });
6247
+ }
6064
6248
  if (fromFile && encoded(fromFile) !== encoded(selected2) || options.port !== undefined && options.port !== selected2.http.port || options.host !== undefined && options.host !== selected2.http.host || options.agentDir !== undefined && resolve4(options.agentDir) !== selected2.agentDir || options.tokenFile !== undefined && resolve4(options.tokenFile) !== selected2.tokenFile || env && selected2.tokenSource !== "environment" || selected2.tokenSource === "environment" && !env || resolveWorkspace(selected2.cwd).root !== workspace.root)
6065
6249
  throw new ContractError("conflict");
6066
6250
  return {
@@ -6211,6 +6395,10 @@ async function stopService(c) {
6211
6395
  function failedStatus(result) {
6212
6396
  return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
6213
6397
  }
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>.
6400
+ `;
6401
+ }
6214
6402
  function startupGuidance(result, input) {
6215
6403
  let report;
6216
6404
  try {
@@ -6229,10 +6417,10 @@ function startupGuidance(result, input) {
6229
6417
  return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
6230
6418
  `;
6231
6419
  case "pi-auth":
6232
- return `${join10(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
6420
+ return `${join11(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
6233
6421
  `;
6234
6422
  case "pi-models":
6235
- return `${join10(agent, "models.json")} must be your own single-link regular file using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
6423
+ return `${join11(agent, "models.json")} must be your own single-link regular file using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
6236
6424
  `;
6237
6425
  case "pi-runtime":
6238
6426
  return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
@@ -6245,15 +6433,26 @@ ${inspect}` : "";
6245
6433
  async function startService(options, foreground, hostUrl) {
6246
6434
  const network = networkSnapshot(process.env);
6247
6435
  const env = hostEnvironment(process.env);
6248
- const input = decodeHostInput(selectStart(options, process.env));
6436
+ let input;
6437
+ try {
6438
+ input = decodeHostInput(selectStart(options, process.env));
6439
+ } catch (error) {
6440
+ if (error instanceof ServerConfigRefusal)
6441
+ process.stderr.write(serverConfigHint(error.path));
6442
+ throw error;
6443
+ }
6249
6444
  const serialized = `${JSON.stringify(input)}
6250
6445
  `;
6251
6446
  if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
6252
6447
  throw new ContractError("oversize");
6253
6448
  const c = new ServiceCoordination(input.workspace, input.stateHome);
6254
6449
  const existing = await serviceStatus(c);
6255
- if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive")
6450
+ if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
6451
+ const running = readHostConfig(c);
6452
+ if (running && encoded(running) !== encoded(input.config))
6453
+ throw new ContractError("conflict");
6256
6454
  return existing;
6455
+ }
6257
6456
  if (!sameNetwork(network, networkSnapshot(process.env)))
6258
6457
  throw new ContractError("stale");
6259
6458
  const child = spawn(process.execPath, [
@@ -6358,6 +6557,14 @@ async function observeStarted(workspace) {
6358
6557
  }
6359
6558
  return { service };
6360
6559
  }
6560
+ async function privateLink(workspace) {
6561
+ try {
6562
+ const value = closed(await nativeCall(new ServiceCoordination(workspace), "token-link", true), ["link"]);
6563
+ return text(value.link, 4096, true);
6564
+ } catch {
6565
+ return;
6566
+ }
6567
+ }
6361
6568
  var ownerControl = {
6362
6569
  async ownerStatus(workspace) {
6363
6570
  try {
@@ -6406,11 +6613,11 @@ async function serverAction(s, action, requested, linger) {
6406
6613
  return { status: "unsupported" };
6407
6614
  }
6408
6615
  }
6409
- function serverGuidance(s, action, result) {
6410
- const cli = piUserInstall(s) ? `node ${JSON.stringify(join10(s.root, "src", "service-cli.mjs"))}` : "cadence";
6616
+ function serverGuidance(s, action, result, link) {
6617
+ const cli = piUserInstall(s) ? `node ${JSON.stringify(join11(s.root, "src", "service-cli.mjs"))}` : "cadence";
6411
6618
  const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
6412
6619
  ` : "";
6413
- return note + serverStatusGuidance(s, action, result, cli);
6620
+ return note + serverStatusGuidance(s, action, result, cli, link);
6414
6621
  }
6415
6622
  function lingerGuidance(linger) {
6416
6623
  switch (linger) {
@@ -6437,10 +6644,11 @@ function managerDetail(s) {
6437
6644
  return `run its launcher to see why: wscript ${JSON.stringify(launcherPath(s))}`;
6438
6645
  }
6439
6646
  }
6440
- function serverStatusGuidance(s, action, result, cli) {
6647
+ function serverStatusGuidance(s, action, result, cli, link) {
6441
6648
  const workspace = JSON.stringify(result.workspace ?? "");
6442
6649
  const manage = `Manage: ${cli} server status|restart|stop|uninstall`;
6443
6650
  const when = s.platform === "linux" ? "at boot or login" : "at login";
6651
+ const config = `Port and token: ${PORT_KEY} and ${TOKEN_KEY} in ${serverConfigPath(s.home)} (edit them, then: ${cli} server restart)`;
6444
6652
  switch (result.status) {
6445
6653
  case "started": {
6446
6654
  const linger = lingerGuidance(result.linger);
@@ -6449,14 +6657,22 @@ function serverStatusGuidance(s, action, result, cli) {
6449
6657
  Inspect: ${cli} diagnose --workspace ${workspace}
6450
6658
  ${manage}
6451
6659
  ${linger}`;
6452
- return `Cadence WebUI: ${result.endpoint} (workspace ${result.workspace}; starts ${when})
6660
+ if (!link)
6661
+ return `Cadence WebUI: ${result.endpoint} (workspace ${result.workspace}; starts ${when})
6453
6662
  Enter with your private link: ${cli} token-link --workspace ${workspace} --reveal
6663
+ ${config}
6664
+ ${manage}
6665
+ ${linger}`;
6666
+ return `Cadence WebUI: ${link}
6667
+ Open this private link (workspace ${result.workspace}; starts ${when}); its token admits anyone who has it.
6668
+ ${config}
6454
6669
  ${manage}
6455
6670
  ${linger}`;
6456
6671
  }
6457
6672
  case "waiting-for-pi-login":
6458
- return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join10(agentDirectory(s), "auth.json")}.
6673
+ return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join11(agentDirectory(s), "auth.json")}.
6459
6674
  Log in to Pi, then: ${cli} server restart
6675
+ ${config}
6460
6676
  ${lingerGuidance(result.linger)}`;
6461
6677
  case "stopped":
6462
6678
  return action === "stop" ? `Cadence stopped; it starts again ${when}, or now with: ${cli} server restart
@@ -6531,9 +6747,21 @@ async function serverMain(argv, hostUrl) {
6531
6747
  }
6532
6748
  if (argv.length)
6533
6749
  throw new ContractError("invalid");
6534
- const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))), ownerControl);
6750
+ const s = hostSystem(dirname9(dirname9(fileURLToPath(hostUrl))), ownerControl);
6535
6751
  if (action === "install")
6536
6752
  process.stderr.write(researchTemplate(s.home));
6753
+ if (action === "install" || action === "restart") {
6754
+ try {
6755
+ const { added } = ensureServerConfig(s.home);
6756
+ 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)}
6758
+ `);
6759
+ } catch (error) {
6760
+ if (error instanceof ServerConfigRefusal)
6761
+ process.stderr.write(serverConfigHint(error.path));
6762
+ throw error;
6763
+ }
6764
+ }
6537
6765
  let result;
6538
6766
  try {
6539
6767
  result = await serverAction(s, action, requested, linger);
@@ -6543,9 +6771,13 @@ async function serverMain(argv, hostUrl) {
6543
6771
  `);
6544
6772
  throw error;
6545
6773
  }
6546
- if (result.status === "started" && result.workspace)
6774
+ let link;
6775
+ if (result.status === "started" && result.workspace) {
6547
6776
  Object.assign(result, await observeStarted(result.workspace));
6548
- process.stderr.write(serverGuidance(s, action, result));
6777
+ if (result.service === "alive")
6778
+ link = await privateLink(result.workspace);
6779
+ }
6780
+ process.stderr.write(serverGuidance(s, action, result, link));
6549
6781
  process.stdout.write(`${JSON.stringify(result)}
6550
6782
  `);
6551
6783
  if ([
@@ -6567,7 +6799,7 @@ async function managementMain(argv, hostUrl) {
6567
6799
  if (command === "--help" && !argv.length) {
6568
6800
  process.stdout.write(`cadence start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
6569
6801
  cadence server install [--workspace PATH] [--no-linger]|status|restart|stop|uninstall
6570
- Start is explicit, or the background service that server install registers: a systemd user unit on Linux, started at boot through linger (--no-linger or CADENCE_LINGER=0 keeps it to login), a LaunchAgent on macOS and a Run entry on Windows, both started at login. Server restart, stop and uninstall use the orderly Stop. cadence-service is the same command. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
6802
+ Start is explicit, or the background service that server install registers: a systemd user unit on Linux, started at boot through linger (--no-linger or CADENCE_LINGER=0 keeps it to login), a LaunchAgent on macOS and a Run entry on Windows, both started at login. Server restart, stop and uninstall use the orderly Stop. cadence-service is the same command. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file, --server-config (the .env whose CADENCE_PORT and CADENCE_TOKEN the background service uses).
6571
6803
  Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
6572
6804
  Recovery resolve requires this local native terminal, exact unchanged dead-owner resources and separate process/credential/storage acknowledgments. No --yes, network recovery, killing, repair or cleanup.
6573
6805
  `);
@@ -6627,6 +6859,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
6627
6859
  options.agentDir = value;
6628
6860
  else if (flag === "--token-file")
6629
6861
  options.tokenFile = value;
6862
+ else if (flag === "--server-config")
6863
+ options.serverConfig = value;
6630
6864
  else if (flag === "--port")
6631
6865
  options.port = portValue(value);
6632
6866
  else if (flag === "--host" && (value === "127.0.0.1" || value === "::1"))