@abelxiaoxing/cadence 2.0.6 → 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 +37 -4
- package/docs/remote-access.md +3 -2
- package/docs/service-management.md +35 -5
- package/package.json +1 -1
- package/src/service/coordination.ts +25 -2
- package/src/service-cli.mjs +829 -504
- package/src/service-host.mjs +378 -103
- package/web/dist/assets/cadence.js +1 -1
- package/web/dist/manifest.json +3 -3
package/src/service-cli.mjs
CHANGED
|
@@ -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
|
|
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,62 @@ 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
|
|
2515
|
-
mkdirSync as
|
|
2516
|
-
readFileSync,
|
|
2517
|
-
realpathSync as
|
|
2518
|
-
renameSync as
|
|
2519
|
-
rmSync,
|
|
2520
|
-
statSync,
|
|
2521
|
-
writeFileSync as
|
|
2522
|
-
} from "node:fs";
|
|
2523
|
-
import { homedir as homedir3 } from "node:os";
|
|
2524
|
-
import {
|
|
2525
|
-
delimiter,
|
|
2526
|
-
dirname as
|
|
2527
|
-
isAbsolute as
|
|
2528
|
-
join as
|
|
2529
|
-
resolve as
|
|
2530
|
-
sep as
|
|
2531
|
-
win32
|
|
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(`
|
|
2693
|
-
`);
|
|
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;
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
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
|
-
}
|
|
2514
|
+
lstatSync as lstatSync4,
|
|
2515
|
+
mkdirSync as mkdirSync4,
|
|
2516
|
+
readFileSync,
|
|
2517
|
+
realpathSync as realpathSync3,
|
|
2518
|
+
renameSync as renameSync3,
|
|
2519
|
+
rmSync,
|
|
2520
|
+
statSync as statSync2,
|
|
2521
|
+
writeFileSync as writeFileSync3
|
|
2522
|
+
} from "node:fs";
|
|
2523
|
+
import { homedir as homedir3 } from "node:os";
|
|
2524
|
+
import {
|
|
2525
|
+
delimiter,
|
|
2526
|
+
dirname as dirname4,
|
|
2527
|
+
isAbsolute as isAbsolute3,
|
|
2528
|
+
join as join4,
|
|
2529
|
+
resolve as resolve3,
|
|
2530
|
+
sep as sep3,
|
|
2531
|
+
win32
|
|
2532
|
+
} from "node:path";
|
|
2874
2533
|
|
|
2875
|
-
// src/service/
|
|
2876
|
-
import { randomBytes
|
|
2877
|
-
import {
|
|
2878
|
-
import {
|
|
2879
|
-
|
|
2534
|
+
// src/service/server-config.ts
|
|
2535
|
+
import { randomBytes } from "node:crypto";
|
|
2536
|
+
import { lstatSync as lstatSync3, realpathSync as realpathSync2, statSync } from "node:fs";
|
|
2537
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2538
|
+
|
|
2539
|
+
// skills/_shared/load-config.mjs
|
|
2540
|
+
function parseEnvFile(content) {
|
|
2541
|
+
const values = {};
|
|
2542
|
+
const lines = content.replaceAll(`\r
|
|
2543
|
+
`, `
|
|
2544
|
+
`).replaceAll("\r", `
|
|
2545
|
+
`).split(`
|
|
2546
|
+
`);
|
|
2547
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
2548
|
+
const line = lines[index].trim();
|
|
2549
|
+
if (!line || line.startsWith("#"))
|
|
2550
|
+
continue;
|
|
2551
|
+
const separator = line.indexOf("=");
|
|
2552
|
+
if (separator <= 0)
|
|
2553
|
+
throw new Error(`Invalid configuration syntax at line ${index + 1}`);
|
|
2554
|
+
const name = line.slice(0, separator).trim();
|
|
2555
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
2556
|
+
throw new Error(`Invalid configuration name at line ${index + 1}`);
|
|
2557
|
+
}
|
|
2558
|
+
let value = line.slice(separator + 1).trim();
|
|
2559
|
+
const quote = value[0];
|
|
2560
|
+
if (quote === '"' || quote === "'") {
|
|
2561
|
+
if (value.at(-1) !== quote) {
|
|
2562
|
+
throw new Error(`Unterminated quoted configuration value at line ${index + 1}`);
|
|
2563
|
+
}
|
|
2564
|
+
value = value.slice(1, -1);
|
|
2565
|
+
}
|
|
2566
|
+
values[name] = value;
|
|
2567
|
+
}
|
|
2568
|
+
return values;
|
|
2569
|
+
}
|
|
2880
2570
|
|
|
2881
2571
|
// src/service/private-files.ts
|
|
2882
2572
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -2885,15 +2575,15 @@ import {
|
|
|
2885
2575
|
constants as constants3,
|
|
2886
2576
|
fstatSync as fstatSync2,
|
|
2887
2577
|
fsyncSync as fsyncSync2,
|
|
2888
|
-
lstatSync as
|
|
2889
|
-
mkdirSync as
|
|
2578
|
+
lstatSync as lstatSync2,
|
|
2579
|
+
mkdirSync as mkdirSync3,
|
|
2890
2580
|
openSync as openSync3,
|
|
2891
2581
|
readSync as readSync2,
|
|
2892
|
-
renameSync as
|
|
2582
|
+
renameSync as renameSync2,
|
|
2893
2583
|
unlinkSync as unlinkSync2,
|
|
2894
|
-
writeFileSync as
|
|
2584
|
+
writeFileSync as writeFileSync2
|
|
2895
2585
|
} from "node:fs";
|
|
2896
|
-
import { dirname as
|
|
2586
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2897
2587
|
var METADATA_BYTES = 16 * 1024;
|
|
2898
2588
|
var REGISTRY_BYTES = 1024 * 1024;
|
|
2899
2589
|
var REGISTRY_ENTRIES = 128;
|
|
@@ -2945,7 +2635,7 @@ class DirectoryWalks {
|
|
|
2945
2635
|
return false;
|
|
2946
2636
|
let st;
|
|
2947
2637
|
try {
|
|
2948
|
-
st =
|
|
2638
|
+
st = lstatSync2(path2);
|
|
2949
2639
|
} catch (error) {
|
|
2950
2640
|
if (!missing(error))
|
|
2951
2641
|
throw error;
|
|
@@ -2970,13 +2660,13 @@ class PrivateFiles {
|
|
|
2970
2660
|
}
|
|
2971
2661
|
directory(path2, create = false, walks) {
|
|
2972
2662
|
const tail = relative2(this.namespace, path2);
|
|
2973
|
-
if (
|
|
2663
|
+
if (isAbsolute2(tail) || tail === ".." || tail.startsWith(`..${sep2}`))
|
|
2974
2664
|
refused();
|
|
2975
2665
|
if (!create) {
|
|
2976
|
-
const full =
|
|
2666
|
+
const full = resolve2(path2);
|
|
2977
2667
|
if (walks?.reuse(full, this.ownerOnly))
|
|
2978
2668
|
return true;
|
|
2979
|
-
const namespace =
|
|
2669
|
+
const namespace = resolve2(this.namespace);
|
|
2980
2670
|
let inside = false, last;
|
|
2981
2671
|
const found = safeDirectory(path2, false, (at2, st) => {
|
|
2982
2672
|
if (at2 === namespace)
|
|
@@ -2991,23 +2681,23 @@ class PrivateFiles {
|
|
|
2991
2681
|
}
|
|
2992
2682
|
if (!safeDirectory(this.namespace, true))
|
|
2993
2683
|
return false;
|
|
2994
|
-
privateMode(
|
|
2684
|
+
privateMode(lstatSync2(this.namespace), this.ownerOnly);
|
|
2995
2685
|
let at = this.namespace;
|
|
2996
|
-
for (const part of tail.split(
|
|
2997
|
-
at =
|
|
2686
|
+
for (const part of tail.split(sep2).filter(Boolean)) {
|
|
2687
|
+
at = join2(at, part);
|
|
2998
2688
|
let st;
|
|
2999
2689
|
try {
|
|
3000
|
-
st =
|
|
2690
|
+
st = lstatSync2(at);
|
|
3001
2691
|
} catch (error) {
|
|
3002
2692
|
if (!missing(error))
|
|
3003
2693
|
throw error;
|
|
3004
2694
|
try {
|
|
3005
|
-
|
|
2695
|
+
mkdirSync3(at, { mode: 448 });
|
|
3006
2696
|
} catch (mkdirError) {
|
|
3007
2697
|
if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
|
|
3008
2698
|
throw mkdirError;
|
|
3009
2699
|
}
|
|
3010
|
-
st =
|
|
2700
|
+
st = lstatSync2(at);
|
|
3011
2701
|
}
|
|
3012
2702
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
3013
2703
|
throw new Error("Unsafe directory");
|
|
@@ -3016,11 +2706,11 @@ class PrivateFiles {
|
|
|
3016
2706
|
return true;
|
|
3017
2707
|
}
|
|
3018
2708
|
read(path2, maximum = METADATA_BYTES, walks) {
|
|
3019
|
-
if (!this.directory(
|
|
2709
|
+
if (!this.directory(dirname2(path2), false, walks))
|
|
3020
2710
|
return;
|
|
3021
2711
|
let before;
|
|
3022
2712
|
try {
|
|
3023
|
-
before =
|
|
2713
|
+
before = lstatSync2(path2);
|
|
3024
2714
|
} catch (error) {
|
|
3025
2715
|
if (missing(error))
|
|
3026
2716
|
return;
|
|
@@ -3043,7 +2733,7 @@ class PrivateFiles {
|
|
|
3043
2733
|
size += n;
|
|
3044
2734
|
}
|
|
3045
2735
|
const bytes = buffer.subarray(0, size);
|
|
3046
|
-
if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened,
|
|
2736
|
+
if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync2(path2)))
|
|
3047
2737
|
refused();
|
|
3048
2738
|
return { bytes, identity: hash(bytes), stamp: stamp(opened) };
|
|
3049
2739
|
} finally {
|
|
@@ -3058,15 +2748,15 @@ class PrivateFiles {
|
|
|
3058
2748
|
create(path2, bytes, maximum = METADATA_BYTES) {
|
|
3059
2749
|
if (Buffer.byteLength(bytes) > maximum)
|
|
3060
2750
|
throw new ContractError("oversize");
|
|
3061
|
-
this.directory(
|
|
2751
|
+
this.directory(dirname2(path2), true);
|
|
3062
2752
|
const fd = retryWindowsFileOperation(() => openSync3(path2, "wx", 384));
|
|
3063
2753
|
try {
|
|
3064
|
-
|
|
2754
|
+
writeFileSync2(fd, bytes);
|
|
3065
2755
|
fsyncSync2(fd);
|
|
3066
2756
|
} finally {
|
|
3067
2757
|
closeSync3(fd);
|
|
3068
2758
|
}
|
|
3069
|
-
this.syncDirectory(
|
|
2759
|
+
this.syncDirectory(dirname2(path2));
|
|
3070
2760
|
const result = this.read(path2, maximum);
|
|
3071
2761
|
if (!result || result.identity !== hash(bytes))
|
|
3072
2762
|
refused();
|
|
@@ -3074,11 +2764,11 @@ class PrivateFiles {
|
|
|
3074
2764
|
}
|
|
3075
2765
|
replace(path2, expected, bytes, maximum = METADATA_BYTES) {
|
|
3076
2766
|
this.assert(path2, expected, maximum);
|
|
3077
|
-
const temporary =
|
|
2767
|
+
const temporary = join2(dirname2(path2), `pending-${randomUUID2()}.json`);
|
|
3078
2768
|
this.create(temporary, bytes, maximum);
|
|
3079
2769
|
this.assert(path2, expected, maximum);
|
|
3080
|
-
retryWindowsFileOperation(() =>
|
|
3081
|
-
this.syncDirectory(
|
|
2770
|
+
retryWindowsFileOperation(() => renameSync2(temporary, path2), () => this.assert(path2, expected, maximum));
|
|
2771
|
+
this.syncDirectory(dirname2(path2));
|
|
3082
2772
|
const result = this.read(path2, maximum);
|
|
3083
2773
|
if (!result || result.identity !== hash(bytes))
|
|
3084
2774
|
refused();
|
|
@@ -3087,7 +2777,7 @@ class PrivateFiles {
|
|
|
3087
2777
|
remove(path2, expected, maximum = METADATA_BYTES) {
|
|
3088
2778
|
this.assert(path2, expected, maximum);
|
|
3089
2779
|
retryWindowsFileOperation(() => unlinkSync2(path2), () => this.assert(path2, expected, maximum));
|
|
3090
|
-
this.syncDirectory(
|
|
2780
|
+
this.syncDirectory(dirname2(path2));
|
|
3091
2781
|
}
|
|
3092
2782
|
syncDirectory(path2) {
|
|
3093
2783
|
if (process.platform === "win32")
|
|
@@ -3100,31 +2790,575 @@ class PrivateFiles {
|
|
|
3100
2790
|
}
|
|
3101
2791
|
}
|
|
3102
2792
|
}
|
|
3103
|
-
function stamp(st) {
|
|
2793
|
+
function stamp(st) {
|
|
2794
|
+
return [
|
|
2795
|
+
st.dev,
|
|
2796
|
+
st.ino,
|
|
2797
|
+
st.size,
|
|
2798
|
+
st.mode,
|
|
2799
|
+
st.uid,
|
|
2800
|
+
st.nlink,
|
|
2801
|
+
st.ctimeMs,
|
|
2802
|
+
st.mtimeMs
|
|
2803
|
+
].join(":");
|
|
2804
|
+
}
|
|
2805
|
+
function json(snapshot, maximum = METADATA_BYTES) {
|
|
2806
|
+
try {
|
|
2807
|
+
return parseWireJson(new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes), maximum);
|
|
2808
|
+
} catch {
|
|
2809
|
+
return refused();
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
function encoded(value) {
|
|
2813
|
+
return `${JSON.stringify(value)}
|
|
2814
|
+
`;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/service/server-config.ts
|
|
2818
|
+
var DEFAULT_PORT = 17423;
|
|
2819
|
+
var PORT_KEY = "CADENCE_PORT";
|
|
2820
|
+
var TOKEN_KEY = "CADENCE_TOKEN";
|
|
2821
|
+
var CONFIG_BYTES = 64 * 1024;
|
|
2822
|
+
var HEADER = [
|
|
2823
|
+
"# Cadence WebUI for `cadence server install`: edit, then run `cadence server restart`.",
|
|
2824
|
+
"# The token admits anyone who has it to your workspace; keep it private."
|
|
2825
|
+
];
|
|
2826
|
+
var TOKEN_BYTES = 1024;
|
|
2827
|
+
function accessToken(value) {
|
|
2828
|
+
const result = text(value, TOKEN_BYTES, true);
|
|
2829
|
+
if (utf8Bytes(result) < 32 || /[\p{Cc}\p{Cf}]/u.test(result))
|
|
2830
|
+
throw new ContractError("invalid");
|
|
2831
|
+
return result;
|
|
2832
|
+
}
|
|
2833
|
+
function serverConfigPath(home) {
|
|
2834
|
+
return join3(home, ".pi", "agent", "cadence", ".env");
|
|
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
|
+
}
|
|
2863
|
+
function settings(source) {
|
|
2864
|
+
try {
|
|
2865
|
+
parseEnvFile(source);
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
unusable(`${syntax(error)}, which the research skills refuse too`);
|
|
2868
|
+
}
|
|
2869
|
+
const parts = source.split(/(\r\n|\r|\n)/);
|
|
2870
|
+
const found = {};
|
|
2871
|
+
for (let at = 0;at < parts.length; at += 2) {
|
|
2872
|
+
const line = parts[at].trim();
|
|
2873
|
+
if (!line || line.startsWith("#"))
|
|
2874
|
+
continue;
|
|
2875
|
+
const separator = line.indexOf("=");
|
|
2876
|
+
const key = line.slice(0, separator).trim();
|
|
2877
|
+
const name = key === PORT_KEY ? "port" : key === TOKEN_KEY ? "token" : undefined;
|
|
2878
|
+
if (!name)
|
|
2879
|
+
continue;
|
|
2880
|
+
const previous = found[name];
|
|
2881
|
+
if (previous)
|
|
2882
|
+
unusable(`sets ${key} twice (lines ${previous.line} and ${at / 2 + 1}); keep one`);
|
|
2883
|
+
let value = line.slice(separator + 1).trim();
|
|
2884
|
+
if (value[0] === '"' || value[0] === "'")
|
|
2885
|
+
value = value.slice(1, -1);
|
|
2886
|
+
found[name] = { at, line: at / 2 + 1, value };
|
|
2887
|
+
}
|
|
2888
|
+
return { parts, ...found };
|
|
2889
|
+
}
|
|
2890
|
+
function decode(source) {
|
|
2891
|
+
const { port, token } = settings(source);
|
|
2892
|
+
if (!port)
|
|
2893
|
+
unusable(`has no ${PORT_KEY} line`);
|
|
2894
|
+
if (!token)
|
|
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
|
+
}
|
|
2905
|
+
}
|
|
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)})`;
|
|
2939
|
+
}
|
|
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
|
+
}
|
|
2959
|
+
function decoded(snapshot) {
|
|
2960
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes);
|
|
2961
|
+
}
|
|
2962
|
+
function refusing(path2, read) {
|
|
2963
|
+
try {
|
|
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)})`);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
function readServerConfig(path2) {
|
|
2972
|
+
return refusing(path2, () => {
|
|
2973
|
+
const file = load(path2);
|
|
2974
|
+
return { ...file, config: decode(file.source) };
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
function ensureServerConfig(home) {
|
|
2978
|
+
const path2 = serverConfigPath(home);
|
|
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);
|
|
2987
|
+
const lines = [];
|
|
2988
|
+
if (!found.port)
|
|
2989
|
+
lines.push(`${PORT_KEY}=${DEFAULT_PORT}`);
|
|
2990
|
+
if (!found.token)
|
|
2991
|
+
lines.push(`${TOKEN_KEY}=${randomBytes(32).toString("base64url")}`);
|
|
2992
|
+
if (lines.length) {
|
|
2993
|
+
const { source } = file2;
|
|
2994
|
+
const eol = source.includes(`\r
|
|
2995
|
+
`) ? `\r
|
|
2996
|
+
` : `
|
|
2997
|
+
`;
|
|
2998
|
+
const ending = !source || /[\r\n]$/.test(source) ? "" : eol;
|
|
2999
|
+
const gap = source ? eol : "";
|
|
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
|
+
}
|
|
3005
|
+
}
|
|
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 };
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// src/service/autostart.ts
|
|
3014
|
+
var UNIT = "cadence.service";
|
|
3015
|
+
var MARKER = "# Generated by @abelxiaoxing/cadence";
|
|
3016
|
+
function wscript(env) {
|
|
3017
|
+
const root = env.SystemRoot;
|
|
3018
|
+
return win32.join(root && win32.isAbsolute(root) ? root : "C:\\Windows", "System32", "wscript.exe");
|
|
3019
|
+
}
|
|
3020
|
+
function hostSystem(root, owner) {
|
|
3021
|
+
const run = (command, args) => {
|
|
3022
|
+
const r = spawnSync2(command, args, {
|
|
3023
|
+
encoding: "utf8",
|
|
3024
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3025
|
+
windowsHide: true
|
|
3026
|
+
});
|
|
3027
|
+
return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
|
|
3028
|
+
};
|
|
3029
|
+
return {
|
|
3030
|
+
platform: process.platform,
|
|
3031
|
+
env: process.env,
|
|
3032
|
+
home: homedir3(),
|
|
3033
|
+
node: process.execPath,
|
|
3034
|
+
root,
|
|
3035
|
+
unit: UNIT,
|
|
3036
|
+
uid: process.getuid?.(),
|
|
3037
|
+
systemctl: (args) => run("systemctl", ["--user", ...args]),
|
|
3038
|
+
linger: () => {
|
|
3039
|
+
const r = run("loginctl", [
|
|
3040
|
+
"show-user",
|
|
3041
|
+
String(process.getuid?.()),
|
|
3042
|
+
"--property=Linger",
|
|
3043
|
+
"--value"
|
|
3044
|
+
]);
|
|
3045
|
+
return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
|
|
3046
|
+
},
|
|
3047
|
+
enableLinger: (interactive) => {
|
|
3048
|
+
const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
|
|
3049
|
+
"--system",
|
|
3050
|
+
"--allow-interactive-authorization=no",
|
|
3051
|
+
"--timeout=15",
|
|
3052
|
+
"call",
|
|
3053
|
+
"org.freedesktop.login1",
|
|
3054
|
+
"/org/freedesktop/login1",
|
|
3055
|
+
"org.freedesktop.login1.Manager",
|
|
3056
|
+
"SetUserLinger",
|
|
3057
|
+
"ubb",
|
|
3058
|
+
String(process.getuid?.()),
|
|
3059
|
+
"true",
|
|
3060
|
+
"false"
|
|
3061
|
+
], { stdio: "ignore", timeout: 20000 });
|
|
3062
|
+
return !r.error && r.status === 0;
|
|
3063
|
+
},
|
|
3064
|
+
launchctl: (args) => run("launchctl", args),
|
|
3065
|
+
reg: (args) => run("reg", args),
|
|
3066
|
+
launch: (path2) => {
|
|
3067
|
+
const r = spawnSync2(wscript(process.env), [path2], {
|
|
3068
|
+
stdio: "ignore",
|
|
3069
|
+
windowsHide: true,
|
|
3070
|
+
timeout: 20000
|
|
3071
|
+
});
|
|
3072
|
+
return !r.error && r.status === 0;
|
|
3073
|
+
},
|
|
3074
|
+
...owner
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
function expandHome(value, home) {
|
|
3078
|
+
if (value === "~")
|
|
3079
|
+
return home;
|
|
3080
|
+
return value.startsWith("~/") ? join4(home, value.slice(2)) : value;
|
|
3081
|
+
}
|
|
3082
|
+
function agentDirectory(s) {
|
|
3083
|
+
const selected = s.env.PI_CODING_AGENT_DIR;
|
|
3084
|
+
return resolve3(selected ? expandHome(selected, s.home) : join4(s.home, ".pi", "agent"));
|
|
3085
|
+
}
|
|
3086
|
+
function piUserInstall(s) {
|
|
3087
|
+
try {
|
|
3088
|
+
const managed = join4(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
|
|
3089
|
+
return realpathSync3(managed) === realpathSync3(s.root);
|
|
3090
|
+
} catch {
|
|
3091
|
+
return false;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
function sdkPackages(root) {
|
|
3095
|
+
let start;
|
|
3096
|
+
try {
|
|
3097
|
+
start = realpathSync3(join4(root, "src"));
|
|
3098
|
+
} catch {
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const found = [];
|
|
3102
|
+
for (const name of ["pi-coding-agent", "pi-ai"]) {
|
|
3103
|
+
let hit;
|
|
3104
|
+
for (let dir = start;!hit; dir = dirname4(dir)) {
|
|
3105
|
+
const candidate = join4(dir, "node_modules", "@earendil-works", name, "package.json");
|
|
3106
|
+
if (regularFile(candidate))
|
|
3107
|
+
hit = candidate;
|
|
3108
|
+
else if (dirname4(dir) === dir)
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
found.push(hit);
|
|
3112
|
+
}
|
|
3113
|
+
return found;
|
|
3114
|
+
}
|
|
3115
|
+
function unitPath(s) {
|
|
3116
|
+
const data = s.env.XDG_DATA_HOME;
|
|
3117
|
+
return join4(data && isAbsolute3(data) ? data : join4(s.home, ".local", "share"), "systemd", "user", s.unit);
|
|
3118
|
+
}
|
|
3119
|
+
function servicePath(value) {
|
|
3120
|
+
const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute3(entry) && !entry.endsWith(`${sep3}node_modules${sep3}.bin`) && !entry.includes(`${sep3}node-gyp-bin`));
|
|
3121
|
+
return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
|
|
3122
|
+
}
|
|
3123
|
+
function serviceEnvironment(s) {
|
|
3124
|
+
const out = [["PATH", servicePath(s.env.PATH)]];
|
|
3125
|
+
for (const key of ["LANG", "LC_ALL"]) {
|
|
3126
|
+
const value = s.env[key];
|
|
3127
|
+
if (value)
|
|
3128
|
+
out.push([key, value]);
|
|
3129
|
+
}
|
|
3130
|
+
const state = s.env.XDG_STATE_HOME;
|
|
3131
|
+
if (state && isAbsolute3(state))
|
|
3132
|
+
out.push(["XDG_STATE_HOME", state]);
|
|
3133
|
+
if (s.env.PI_CODING_AGENT_DIR)
|
|
3134
|
+
out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
|
|
3135
|
+
return out;
|
|
3136
|
+
}
|
|
3137
|
+
function checked(value) {
|
|
3138
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
3139
|
+
throw new ContractError("invalid");
|
|
3140
|
+
return value;
|
|
3141
|
+
}
|
|
3142
|
+
var specifiers = (value) => checked(value).replaceAll("%", "%%");
|
|
3143
|
+
var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
|
|
3144
|
+
var argument = (value) => quoted(value).replaceAll("$", "$$$$");
|
|
3145
|
+
function buildUnit(u) {
|
|
3146
|
+
const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
|
|
3104
3147
|
return [
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3148
|
+
`${MARKER}; \`cadence server install\` and \`restart\` rewrite this file.`,
|
|
3149
|
+
"# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
|
|
3150
|
+
"[Unit]",
|
|
3151
|
+
"Description=Cadence shared task service and WebUI",
|
|
3152
|
+
"Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
|
|
3153
|
+
`ConditionPathExists=${specifiers(u.cli)}`,
|
|
3154
|
+
`ConditionPathExists=${specifiers(join4(u.agentDir, "auth.json"))}`,
|
|
3155
|
+
...u.sdk.map((path2) => `ConditionPathExists=${specifiers(path2)}`),
|
|
3156
|
+
"StartLimitIntervalSec=300",
|
|
3157
|
+
"StartLimitBurst=3",
|
|
3158
|
+
"",
|
|
3159
|
+
"[Service]",
|
|
3160
|
+
"Type=simple",
|
|
3161
|
+
`WorkingDirectory=${specifiers(u.workspace)}`,
|
|
3162
|
+
...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
|
|
3163
|
+
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace, "--server-config", u.serverConfig])}`,
|
|
3164
|
+
`ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
|
|
3165
|
+
"KillMode=mixed",
|
|
3166
|
+
"TimeoutStopSec=90",
|
|
3167
|
+
"Restart=on-failure",
|
|
3168
|
+
"RestartSec=10",
|
|
3169
|
+
"",
|
|
3170
|
+
"[Install]",
|
|
3171
|
+
"WantedBy=default.target",
|
|
3172
|
+
""
|
|
3173
|
+
].join(`
|
|
3174
|
+
`);
|
|
3114
3175
|
}
|
|
3115
|
-
function
|
|
3176
|
+
function readGenerated(path2, marker) {
|
|
3116
3177
|
try {
|
|
3117
|
-
|
|
3178
|
+
if (!lstatSync4(path2).isFile())
|
|
3179
|
+
return null;
|
|
3180
|
+
const bytes = readFileSync(path2);
|
|
3181
|
+
const text2 = bytes[0] === 255 && bytes[1] === 254 ? bytes.subarray(2).toString("utf16le") : bytes.toString("utf8");
|
|
3182
|
+
return text2.startsWith(marker) ? text2 : null;
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
if (error.code === "ENOENT")
|
|
3185
|
+
return;
|
|
3186
|
+
throw error;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function unitWorkspace(text2) {
|
|
3190
|
+
return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
|
|
3191
|
+
}
|
|
3192
|
+
function directory(path2) {
|
|
3193
|
+
try {
|
|
3194
|
+
return isAbsolute3(path2) && statSync2(path2).isDirectory();
|
|
3118
3195
|
} catch {
|
|
3119
|
-
return
|
|
3196
|
+
return false;
|
|
3120
3197
|
}
|
|
3121
3198
|
}
|
|
3122
|
-
function
|
|
3123
|
-
|
|
3124
|
-
|
|
3199
|
+
function serviceStateHome(s) {
|
|
3200
|
+
const state = s.platform === "win32" ? s.env.LOCALAPPDATA : s.env.XDG_STATE_HOME;
|
|
3201
|
+
return state && isAbsolute3(state) ? state : join4(s.home, ...s.platform === "win32" ? ["AppData", "Local"] : [".local", "state"]);
|
|
3202
|
+
}
|
|
3203
|
+
function selectWorkspace(s, requested, kept) {
|
|
3204
|
+
let chosen;
|
|
3205
|
+
if (requested) {
|
|
3206
|
+
chosen = expandHome(requested, s.home);
|
|
3207
|
+
if (!directory(chosen))
|
|
3208
|
+
throw new ContractError("invalid");
|
|
3209
|
+
chosen = resolve3(chosen);
|
|
3210
|
+
} else if (kept && directory(kept))
|
|
3211
|
+
chosen = kept;
|
|
3212
|
+
const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace: join4(s.home, LANDING_WORKSPACE), redirected: false };
|
|
3213
|
+
if (landing.workspace !== chosen)
|
|
3214
|
+
mkdirSync4(landing.workspace, { recursive: true });
|
|
3215
|
+
return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
|
|
3216
|
+
}
|
|
3217
|
+
function write(path2, content) {
|
|
3218
|
+
mkdirSync4(dirname4(path2), { recursive: true });
|
|
3219
|
+
const temporary = `${path2}.${process.pid}.tmp`;
|
|
3220
|
+
try {
|
|
3221
|
+
writeFileSync3(temporary, content, { mode: 420, flag: "wx" });
|
|
3222
|
+
renameSync3(temporary, path2);
|
|
3223
|
+
} finally {
|
|
3224
|
+
rmSync(temporary, { force: true });
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
function regularFile(path2) {
|
|
3228
|
+
try {
|
|
3229
|
+
return statSync2(path2).isFile();
|
|
3230
|
+
} catch {
|
|
3231
|
+
return false;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
async function settle(s, workspace) {
|
|
3235
|
+
if (!workspace)
|
|
3236
|
+
return;
|
|
3237
|
+
const status = await s.stopOwner(workspace);
|
|
3238
|
+
if (status === "stopped")
|
|
3239
|
+
return;
|
|
3240
|
+
return {
|
|
3241
|
+
status: status === "quarantined" || status === "unavailable" ? status : "unconfirmed",
|
|
3242
|
+
workspace
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
function bootStart(s, attempt) {
|
|
3246
|
+
const current = s.linger();
|
|
3247
|
+
if (current === undefined)
|
|
3248
|
+
return {};
|
|
3249
|
+
if (current)
|
|
3250
|
+
return { linger: "on" };
|
|
3251
|
+
if (!attempt)
|
|
3252
|
+
return { linger: "off" };
|
|
3253
|
+
return {
|
|
3254
|
+
linger: s.enableLinger(false) || s.enableLinger(true) ? "enabled" : "refused"
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
function configureAutostart(s, mode, requested, linger = false) {
|
|
3258
|
+
if (s.platform !== "linux")
|
|
3259
|
+
return { status: "unsupported" };
|
|
3260
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
3261
|
+
return { status: "unavailable" };
|
|
3262
|
+
const path2 = unitPath(s);
|
|
3263
|
+
const previous = readGenerated(path2, MARKER);
|
|
3264
|
+
if (previous === null)
|
|
3265
|
+
return { status: "foreign", unit: path2 };
|
|
3266
|
+
if (mode === "restart" && previous === undefined)
|
|
3267
|
+
return { status: "absent" };
|
|
3268
|
+
const sdk = sdkPackages(s.root);
|
|
3269
|
+
if (!sdk)
|
|
3270
|
+
return { status: "missing-pi-sdk" };
|
|
3271
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested, unitWorkspace(previous));
|
|
3272
|
+
const agentDir = agentDirectory(s);
|
|
3273
|
+
const content = buildUnit({
|
|
3274
|
+
node: s.node,
|
|
3275
|
+
cli: join4(s.root, "src", "service-cli.mjs"),
|
|
3276
|
+
workspace,
|
|
3277
|
+
serverConfig: serverConfigPath(s.home),
|
|
3278
|
+
agentDir,
|
|
3279
|
+
sdk,
|
|
3280
|
+
environment: serviceEnvironment(s)
|
|
3281
|
+
});
|
|
3282
|
+
const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
|
|
3283
|
+
const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
|
|
3284
|
+
if (active)
|
|
3285
|
+
s.systemctl(["stop", s.unit]);
|
|
3286
|
+
write(path2, content);
|
|
3287
|
+
s.systemctl(["daemon-reload"]);
|
|
3288
|
+
const result = {
|
|
3289
|
+
unit: path2,
|
|
3290
|
+
workspace,
|
|
3291
|
+
...redirectedFrom ? { redirectedFrom } : {}
|
|
3292
|
+
};
|
|
3293
|
+
if (enabled.startsWith("masked"))
|
|
3294
|
+
return { status: "masked", ...result };
|
|
3295
|
+
if (mode === "install" && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
3296
|
+
return { status: "failed", ...result };
|
|
3297
|
+
const boot = () => mode === "install" ? bootStart(s, linger) : {};
|
|
3298
|
+
if (!regularFile(join4(agentDir, "auth.json")))
|
|
3299
|
+
return { status: "waiting-for-pi-login", ...result, ...boot() };
|
|
3300
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
3301
|
+
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
3302
|
+
return { status: started ? "started" : "failed", ...result, ...boot() };
|
|
3303
|
+
}
|
|
3304
|
+
function stopAutostart(s) {
|
|
3305
|
+
if (s.platform !== "linux")
|
|
3306
|
+
return { status: "unsupported" };
|
|
3307
|
+
const path2 = unitPath(s);
|
|
3308
|
+
const previous = readGenerated(path2, MARKER);
|
|
3309
|
+
if (previous === null)
|
|
3310
|
+
return { status: "foreign", unit: path2 };
|
|
3311
|
+
if (previous === undefined)
|
|
3312
|
+
return { status: "absent" };
|
|
3313
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
3314
|
+
return { status: "unavailable", unit: path2 };
|
|
3315
|
+
return {
|
|
3316
|
+
status: s.systemctl(["stop", s.unit]).status === 0 ? "stopped" : "failed",
|
|
3317
|
+
unit: path2,
|
|
3318
|
+
workspace: unitWorkspace(previous)
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
function removeAutostart(s) {
|
|
3322
|
+
if (s.platform !== "linux")
|
|
3323
|
+
return { status: "unsupported" };
|
|
3324
|
+
const path2 = unitPath(s);
|
|
3325
|
+
const previous = readGenerated(path2, MARKER);
|
|
3326
|
+
if (previous === null)
|
|
3327
|
+
return { status: "foreign", unit: path2 };
|
|
3328
|
+
if (previous === undefined)
|
|
3329
|
+
return { status: "absent" };
|
|
3330
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
3331
|
+
return { status: "unavailable", unit: path2 };
|
|
3332
|
+
if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
|
|
3333
|
+
return { status: "failed", unit: path2 };
|
|
3334
|
+
rmSync(path2);
|
|
3335
|
+
s.systemctl(["daemon-reload"]);
|
|
3336
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
3337
|
+
return { status: "removed", unit: path2 };
|
|
3338
|
+
}
|
|
3339
|
+
function autostartStatus(s) {
|
|
3340
|
+
if (s.platform !== "linux")
|
|
3341
|
+
return { status: "unsupported" };
|
|
3342
|
+
const path2 = unitPath(s);
|
|
3343
|
+
const previous = readGenerated(path2, MARKER);
|
|
3344
|
+
if (previous === null)
|
|
3345
|
+
return { status: "foreign", unit: path2 };
|
|
3346
|
+
if (previous === undefined)
|
|
3347
|
+
return { status: "absent" };
|
|
3348
|
+
return {
|
|
3349
|
+
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
3350
|
+
enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
|
|
3351
|
+
unit: path2,
|
|
3352
|
+
workspace: unitWorkspace(previous),
|
|
3353
|
+
...bootStart(s, false)
|
|
3354
|
+
};
|
|
3125
3355
|
}
|
|
3126
3356
|
|
|
3127
3357
|
// src/service/coordination.ts
|
|
3358
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
|
|
3359
|
+
import { opendirSync, realpathSync as realpathSync4 } from "node:fs";
|
|
3360
|
+
import { hostname as hostname2 } from "node:os";
|
|
3361
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join5, relative as relative3, sep as sep4 } from "node:path";
|
|
3128
3362
|
var registrationKeys = [
|
|
3129
3363
|
"schema",
|
|
3130
3364
|
"root",
|
|
@@ -3250,17 +3484,17 @@ class ServiceCoordination {
|
|
|
3250
3484
|
stateHome;
|
|
3251
3485
|
constructor(workspace, stateHome) {
|
|
3252
3486
|
this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
|
|
3253
|
-
const namespace =
|
|
3487
|
+
const namespace = dirname5(this.store.privateRoot);
|
|
3254
3488
|
this.files = new PrivateFiles(namespace);
|
|
3255
|
-
this.stateHome =
|
|
3256
|
-
this.directory =
|
|
3257
|
-
this.registryDirectory =
|
|
3489
|
+
this.stateHome = dirname5(namespace);
|
|
3490
|
+
this.directory = join5(this.store.privateRoot, "service-v1");
|
|
3491
|
+
this.registryDirectory = join5(namespace, "service-registry-v1");
|
|
3258
3492
|
}
|
|
3259
3493
|
path(name) {
|
|
3260
|
-
return
|
|
3494
|
+
return join5(this.directory, name);
|
|
3261
3495
|
}
|
|
3262
3496
|
registryPath(name) {
|
|
3263
|
-
return
|
|
3497
|
+
return join5(this.registryDirectory, name);
|
|
3264
3498
|
}
|
|
3265
3499
|
registry() {
|
|
3266
3500
|
if (!this.files.directory(this.registryDirectory))
|
|
@@ -3280,7 +3514,7 @@ class ServiceCoordination {
|
|
|
3280
3514
|
const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
|
|
3281
3515
|
const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
|
|
3282
3516
|
for (const [i, row] of entries.entries()) {
|
|
3283
|
-
if (row.stateHome !== this.stateHome ||
|
|
3517
|
+
if (row.stateHome !== this.stateHome || realpathSync4(row.root) !== row.root)
|
|
3284
3518
|
refused();
|
|
3285
3519
|
if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
|
|
3286
3520
|
refused();
|
|
@@ -3308,7 +3542,7 @@ class ServiceCoordination {
|
|
|
3308
3542
|
"host-config.json",
|
|
3309
3543
|
"history-v2"
|
|
3310
3544
|
]);
|
|
3311
|
-
this.files.directory(
|
|
3545
|
+
this.files.directory(join5(this.directory, "history-v2"), false, walks);
|
|
3312
3546
|
}
|
|
3313
3547
|
selected() {
|
|
3314
3548
|
const walks = new DirectoryWalks;
|
|
@@ -3468,7 +3702,7 @@ class ServiceCoordination {
|
|
|
3468
3702
|
throw error;
|
|
3469
3703
|
}
|
|
3470
3704
|
}
|
|
3471
|
-
acquire(input, resolvedEndpoint = input.endpoint, initializeNew) {
|
|
3705
|
+
acquire(input, resolvedEndpoint = input.endpoint, initializeNew, reselectExisting) {
|
|
3472
3706
|
const config = decodeOwnerConfig(input);
|
|
3473
3707
|
const bound = endpoint(resolvedEndpoint);
|
|
3474
3708
|
if (!endpointMatches(config.endpoint, bound))
|
|
@@ -3483,7 +3717,8 @@ class ServiceCoordination {
|
|
|
3483
3717
|
throw new ContractError("quarantine");
|
|
3484
3718
|
if (this.store.inspectLock())
|
|
3485
3719
|
throw new ContractError("conflict");
|
|
3486
|
-
|
|
3720
|
+
const reselect = !!old && old.config.identity !== hash(encoded(config));
|
|
3721
|
+
if (reselect && (!reselectExisting || !old?.hostConfig))
|
|
3487
3722
|
throw new ContractError("conflict");
|
|
3488
3723
|
const row = {
|
|
3489
3724
|
root: this.store.root,
|
|
@@ -3500,7 +3735,17 @@ class ServiceCoordination {
|
|
|
3500
3735
|
...registry.value,
|
|
3501
3736
|
entries: [...registry.value.entries, row]
|
|
3502
3737
|
}), REGISTRY_BYTES);
|
|
3503
|
-
|
|
3738
|
+
if (old?.hostConfig && reselect && reselectExisting) {
|
|
3739
|
+
reselectExisting(old.hostConfig);
|
|
3740
|
+
const saved = this.files.replace(this.path("config.json"), old.config, encoded(config));
|
|
3741
|
+
this.files.replace(this.path("format.json"), old.manifest, encoded({
|
|
3742
|
+
schema: "cadence-service-format/1",
|
|
3743
|
+
root: this.store.root,
|
|
3744
|
+
stateHome: this.stateHome,
|
|
3745
|
+
configIdentity: saved.identity
|
|
3746
|
+
}));
|
|
3747
|
+
} else
|
|
3748
|
+
this.initialize(config);
|
|
3504
3749
|
if (!old)
|
|
3505
3750
|
initializeNew?.();
|
|
3506
3751
|
const checkout = this.store.acquire();
|
|
@@ -3517,7 +3762,7 @@ class ServiceCoordination {
|
|
|
3517
3762
|
const capability = this.files.create(this.path("local-capability.json"), encoded({
|
|
3518
3763
|
schema: "cadence-service-local-capability/1",
|
|
3519
3764
|
epoch: row.epoch,
|
|
3520
|
-
capability:
|
|
3765
|
+
capability: randomBytes2(32).toString("base64url")
|
|
3521
3766
|
}));
|
|
3522
3767
|
const value = {
|
|
3523
3768
|
schema: "cadence-service-registration/1",
|
|
@@ -3730,12 +3975,12 @@ class OwnerLease {
|
|
|
3730
3975
|
|
|
3731
3976
|
// src/service/launchd.ts
|
|
3732
3977
|
import { rmSync as rmSync2 } from "node:fs";
|
|
3733
|
-
import { join as
|
|
3978
|
+
import { join as join6 } from "node:path";
|
|
3734
3979
|
var LABEL = "com.abelxiaoxing.cadence";
|
|
3735
3980
|
var MARKER2 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3736
3981
|
<!-- Generated by @abelxiaoxing/cadence`;
|
|
3737
3982
|
function plistPath(s) {
|
|
3738
|
-
return
|
|
3983
|
+
return join6(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
3739
3984
|
}
|
|
3740
3985
|
var xml = (value) => checked(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
3741
3986
|
function buildPlist(u) {
|
|
@@ -3750,7 +3995,16 @@ function buildPlist(u) {
|
|
|
3750
3995
|
` <string>${LABEL}</string>`,
|
|
3751
3996
|
"\t<key>ProgramArguments</key>",
|
|
3752
3997
|
"\t<array>",
|
|
3753
|
-
...[
|
|
3998
|
+
...[
|
|
3999
|
+
u.node,
|
|
4000
|
+
u.cli,
|
|
4001
|
+
"start",
|
|
4002
|
+
"--foreground",
|
|
4003
|
+
"--workspace",
|
|
4004
|
+
u.workspace,
|
|
4005
|
+
"--server-config",
|
|
4006
|
+
u.serverConfig
|
|
4007
|
+
].map(string),
|
|
3754
4008
|
"\t</array>",
|
|
3755
4009
|
"\t<key>WorkingDirectory</key>",
|
|
3756
4010
|
` <string>${xml(u.workspace)}</string>`,
|
|
@@ -3806,10 +4060,11 @@ async function configureLaunchAgent(s, mode, requested) {
|
|
|
3806
4060
|
s.launchctl(["bootout", target(s)]);
|
|
3807
4061
|
write(path2, buildPlist({
|
|
3808
4062
|
node: s.node,
|
|
3809
|
-
cli:
|
|
4063
|
+
cli: join6(s.root, "src", "service-cli.mjs"),
|
|
3810
4064
|
workspace,
|
|
4065
|
+
serverConfig: serverConfigPath(s.home),
|
|
3811
4066
|
environment: serviceEnvironment(s),
|
|
3812
|
-
log:
|
|
4067
|
+
log: join6(s.home, "Library", "Logs", "cadence.log")
|
|
3813
4068
|
}));
|
|
3814
4069
|
const result = {
|
|
3815
4070
|
unit: path2,
|
|
@@ -3820,7 +4075,7 @@ async function configureLaunchAgent(s, mode, requested) {
|
|
|
3820
4075
|
s.launchctl(["enable", target(s)]);
|
|
3821
4076
|
else if (disabled(s))
|
|
3822
4077
|
return { status: "disabled", ...result };
|
|
3823
|
-
if (!regularFile(
|
|
4078
|
+
if (!regularFile(join6(agentDirectory(s), "auth.json")))
|
|
3824
4079
|
return { status: "waiting-for-pi-login", ...result };
|
|
3825
4080
|
const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
|
|
3826
4081
|
return { status: started ? "started" : "failed", ...result };
|
|
@@ -3869,14 +4124,14 @@ function launchAgentStatus(s) {
|
|
|
3869
4124
|
|
|
3870
4125
|
// src/service/login-run.ts
|
|
3871
4126
|
import { rmSync as rmSync3 } from "node:fs";
|
|
3872
|
-
import { isAbsolute as isAbsolute5, join as
|
|
4127
|
+
import { isAbsolute as isAbsolute5, join as join7 } from "node:path";
|
|
3873
4128
|
var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
3874
4129
|
var APPROVED_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run";
|
|
3875
4130
|
var RUN_VALUE = "Cadence";
|
|
3876
4131
|
var MARKER3 = "' Generated by @abelxiaoxing/cadence";
|
|
3877
4132
|
function launcherPath(s) {
|
|
3878
4133
|
const local = s.env.LOCALAPPDATA;
|
|
3879
|
-
return
|
|
4134
|
+
return join7(local && isAbsolute5(local) ? local : join7(s.home, "AppData", "Local"), "cadence-server", "cadence.vbs");
|
|
3880
4135
|
}
|
|
3881
4136
|
var literal2 = (value) => `"${checked(value).replaceAll('"', '""')}"`;
|
|
3882
4137
|
function argument2(value) {
|
|
@@ -3885,7 +4140,7 @@ function argument2(value) {
|
|
|
3885
4140
|
return `"${value}"`;
|
|
3886
4141
|
}
|
|
3887
4142
|
function buildLauncher(u) {
|
|
3888
|
-
const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)}`;
|
|
4143
|
+
const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)} --server-config ${argument2(u.serverConfig)}`;
|
|
3889
4144
|
return [
|
|
3890
4145
|
`${MARKER3}; \`cadence server install\` and \`restart\` rewrite this file.`,
|
|
3891
4146
|
`' Started at login by ${RUN_KEY}\\${RUN_VALUE}; window style 0 hides the console.`,
|
|
@@ -3929,8 +4184,9 @@ async function configureLoginRun(s, mode, requested) {
|
|
|
3929
4184
|
const environment = s.env.PI_CODING_AGENT_DIR ? [["PI_CODING_AGENT_DIR", agentDirectory(s)]] : [];
|
|
3930
4185
|
write(path2, utf16(buildLauncher({
|
|
3931
4186
|
node: s.node,
|
|
3932
|
-
cli:
|
|
4187
|
+
cli: join7(s.root, "src", "service-cli.mjs"),
|
|
3933
4188
|
workspace,
|
|
4189
|
+
serverConfig: serverConfigPath(s.home),
|
|
3934
4190
|
environment
|
|
3935
4191
|
})));
|
|
3936
4192
|
const result = {
|
|
@@ -3944,7 +4200,7 @@ async function configureLoginRun(s, mode, requested) {
|
|
|
3944
4200
|
return { status: "failed", ...result };
|
|
3945
4201
|
s.reg(["delete", APPROVED_KEY, "/v", RUN_VALUE, "/f"]);
|
|
3946
4202
|
}
|
|
3947
|
-
if (!regularFile(
|
|
4203
|
+
if (!regularFile(join7(agentDirectory(s), "auth.json")))
|
|
3948
4204
|
return { status: "waiting-for-pi-login", ...result };
|
|
3949
4205
|
return { status: s.launch(path2) ? "started" : "failed", ...result };
|
|
3950
4206
|
}
|
|
@@ -3997,8 +4253,8 @@ import { isatty } from "node:tty";
|
|
|
3997
4253
|
|
|
3998
4254
|
// src/history/recovery.ts
|
|
3999
4255
|
import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
|
|
4000
|
-
import { lstatSync as
|
|
4001
|
-
import { join as
|
|
4256
|
+
import { lstatSync as lstatSync7, opendirSync as opendirSync3 } from "node:fs";
|
|
4257
|
+
import { join as join9, relative as relative4 } from "node:path";
|
|
4002
4258
|
|
|
4003
4259
|
// src/service/recovery.ts
|
|
4004
4260
|
async function inspectRecovery(coordination, history) {
|
|
@@ -4046,19 +4302,19 @@ async function resolveRecovery(coordination, confirmation, history) {
|
|
|
4046
4302
|
}
|
|
4047
4303
|
|
|
4048
4304
|
// src/history/files.ts
|
|
4049
|
-
import { createHash as createHash4, randomBytes as
|
|
4305
|
+
import { createHash as createHash4, randomBytes as randomBytes3, randomUUID as randomUUID4 } from "node:crypto";
|
|
4050
4306
|
import {
|
|
4051
4307
|
closeSync as closeSync5,
|
|
4052
4308
|
constants as constants5,
|
|
4053
4309
|
fstatSync as fstatSync4,
|
|
4054
4310
|
fsyncSync as fsyncSync3,
|
|
4055
|
-
lstatSync as
|
|
4311
|
+
lstatSync as lstatSync6,
|
|
4056
4312
|
opendirSync as opendirSync2,
|
|
4057
4313
|
openSync as openSync5,
|
|
4058
4314
|
statfsSync
|
|
4059
4315
|
} from "node:fs";
|
|
4060
4316
|
import { open } from "node:fs/promises";
|
|
4061
|
-
import { dirname as
|
|
4317
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
4062
4318
|
|
|
4063
4319
|
// src/history/admission.ts
|
|
4064
4320
|
function decodeRunAdmission(value) {
|
|
@@ -4082,19 +4338,19 @@ import {
|
|
|
4082
4338
|
closeSync as closeSync4,
|
|
4083
4339
|
constants as constants4,
|
|
4084
4340
|
fstatSync as fstatSync3,
|
|
4085
|
-
lstatSync as
|
|
4341
|
+
lstatSync as lstatSync5,
|
|
4086
4342
|
openSync as openSync4,
|
|
4087
4343
|
readSync as readSync3
|
|
4088
4344
|
} from "node:fs";
|
|
4089
|
-
import { dirname as
|
|
4345
|
+
import { dirname as dirname6 } from "node:path";
|
|
4090
4346
|
function bad() {
|
|
4091
4347
|
throw new ContractError("corrupt-selected");
|
|
4092
4348
|
}
|
|
4093
4349
|
var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
|
|
4094
4350
|
function selected(files, path2, read, walks) {
|
|
4095
|
-
if (!files.files.directory(
|
|
4351
|
+
if (!files.files.directory(dirname6(path2), false, walks))
|
|
4096
4352
|
bad();
|
|
4097
|
-
const before =
|
|
4353
|
+
const before = lstatSync5(path2);
|
|
4098
4354
|
if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
|
|
4099
4355
|
bad();
|
|
4100
4356
|
const fd = openSync4(path2, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
@@ -4102,7 +4358,7 @@ function selected(files, path2, read, walks) {
|
|
|
4102
4358
|
if (stamp2(fstatSync3(fd)) !== stamp2(before))
|
|
4103
4359
|
bad();
|
|
4104
4360
|
const result = read(fd, before.size, stamp2(before));
|
|
4105
|
-
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(
|
|
4361
|
+
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync5(path2)) !== stamp2(before))
|
|
4106
4362
|
bad();
|
|
4107
4363
|
return result;
|
|
4108
4364
|
} finally {
|
|
@@ -4589,14 +4845,14 @@ class HistoryFiles {
|
|
|
4589
4845
|
this.coordination = coordination;
|
|
4590
4846
|
this.format = format;
|
|
4591
4847
|
this.formatSnapshot = formatSnapshot;
|
|
4592
|
-
this.directory =
|
|
4848
|
+
this.directory = join8(coordination.directory, "history-v2");
|
|
4593
4849
|
this.files = coordination.files;
|
|
4594
4850
|
}
|
|
4595
4851
|
static open(coordination) {
|
|
4596
|
-
const dir =
|
|
4852
|
+
const dir = join8(coordination.directory, "history-v2");
|
|
4597
4853
|
if (!coordination.files.directory(dir))
|
|
4598
4854
|
return;
|
|
4599
|
-
const file = coordination.files.read(
|
|
4855
|
+
const file = coordination.files.read(join8(dir, "format.json"));
|
|
4600
4856
|
if (!file)
|
|
4601
4857
|
corrupt();
|
|
4602
4858
|
const format = decodeFormat(json(file));
|
|
@@ -4612,7 +4868,7 @@ class HistoryFiles {
|
|
|
4612
4868
|
existing.assertWritable();
|
|
4613
4869
|
return existing;
|
|
4614
4870
|
}
|
|
4615
|
-
const dir =
|
|
4871
|
+
const dir = join8(c.directory, "history-v2");
|
|
4616
4872
|
const format = {
|
|
4617
4873
|
schema: FORMAT,
|
|
4618
4874
|
storeId: randomUUID4(),
|
|
@@ -4622,19 +4878,19 @@ class HistoryFiles {
|
|
|
4622
4878
|
c.files.directory(dir, true);
|
|
4623
4879
|
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
4624
4880
|
lease.assert();
|
|
4625
|
-
c.files.directory(
|
|
4881
|
+
c.files.directory(join8(dir, name), true);
|
|
4626
4882
|
}
|
|
4627
4883
|
lease.assert();
|
|
4628
|
-
const keyPath =
|
|
4884
|
+
const keyPath = join8(dir, "read-proof-key.json");
|
|
4629
4885
|
c.files.create(keyPath, encoded({
|
|
4630
4886
|
schema: "cadence-history-read-key/1",
|
|
4631
4887
|
storeId: format.storeId,
|
|
4632
|
-
key:
|
|
4888
|
+
key: randomBytes3(32).toString("base64url")
|
|
4633
4889
|
}));
|
|
4634
4890
|
lease.assert();
|
|
4635
|
-
const snapshot = c.files.create(
|
|
4891
|
+
const snapshot = c.files.create(join8(dir, "format.json"), encoded(format));
|
|
4636
4892
|
const result = new HistoryFiles(c, format, snapshot);
|
|
4637
|
-
result.syncParents(
|
|
4893
|
+
result.syncParents(join8(dir, "format.json"));
|
|
4638
4894
|
result.assertWritable();
|
|
4639
4895
|
return result;
|
|
4640
4896
|
}
|
|
@@ -4669,7 +4925,7 @@ class HistoryFiles {
|
|
|
4669
4925
|
throw new ContractError("quarantine");
|
|
4670
4926
|
}
|
|
4671
4927
|
assertFormat() {
|
|
4672
|
-
this.files.assert(
|
|
4928
|
+
this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot);
|
|
4673
4929
|
}
|
|
4674
4930
|
readProofMaterial(budget) {
|
|
4675
4931
|
if (this.format.schema !== FORMAT)
|
|
@@ -4678,7 +4934,7 @@ class HistoryFiles {
|
|
|
4678
4934
|
this.assertReadFormat(budget);
|
|
4679
4935
|
else
|
|
4680
4936
|
this.assertFormat();
|
|
4681
|
-
const selected2 = this.readBudgeted(
|
|
4937
|
+
const selected2 = this.readBudgeted(join8(this.directory, "read-proof-key.json"), 512, budget);
|
|
4682
4938
|
if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
|
|
4683
4939
|
corrupt();
|
|
4684
4940
|
try {
|
|
@@ -4706,17 +4962,17 @@ class HistoryFiles {
|
|
|
4706
4962
|
return;
|
|
4707
4963
|
}
|
|
4708
4964
|
this.assertFormat();
|
|
4709
|
-
assertIncarnation(this,
|
|
4965
|
+
assertIncarnation(this, join8(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
4710
4966
|
}
|
|
4711
4967
|
segmentPath(segment) {
|
|
4712
|
-
return
|
|
4968
|
+
return join8(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
4713
4969
|
}
|
|
4714
4970
|
artifactPath(ref) {
|
|
4715
|
-
return
|
|
4971
|
+
return join8(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
|
|
4716
4972
|
}
|
|
4717
4973
|
admissionPath(runId) {
|
|
4718
4974
|
decodeId(runId);
|
|
4719
|
-
return
|
|
4975
|
+
return join8(this.directory, "runs", runId, "admission.json");
|
|
4720
4976
|
}
|
|
4721
4977
|
writeAdmission(lease, input) {
|
|
4722
4978
|
lease.assert();
|
|
@@ -4730,7 +4986,7 @@ class HistoryFiles {
|
|
|
4730
4986
|
return this.files.read(path2, maximum);
|
|
4731
4987
|
let size;
|
|
4732
4988
|
try {
|
|
4733
|
-
size =
|
|
4989
|
+
size = lstatSync6(path2).size;
|
|
4734
4990
|
} catch {
|
|
4735
4991
|
return this.files.read(path2, 0, budget.walks);
|
|
4736
4992
|
}
|
|
@@ -4742,7 +4998,7 @@ class HistoryFiles {
|
|
|
4742
4998
|
readCommand(commandId, budget) {
|
|
4743
4999
|
decodeId(commandId);
|
|
4744
5000
|
this.assertReadFormat(budget);
|
|
4745
|
-
return this.readBudgeted(
|
|
5001
|
+
return this.readBudgeted(join8(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
|
|
4746
5002
|
}
|
|
4747
5003
|
readAdmission(runId, identity, budget) {
|
|
4748
5004
|
return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
|
|
@@ -4763,7 +5019,7 @@ class HistoryFiles {
|
|
|
4763
5019
|
syncParents(file) {
|
|
4764
5020
|
if (process.platform === "win32")
|
|
4765
5021
|
return;
|
|
4766
|
-
for (let path2 =
|
|
5022
|
+
for (let path2 = dirname7(file);; path2 = dirname7(path2)) {
|
|
4767
5023
|
if (!this.files.directory(path2))
|
|
4768
5024
|
corrupt();
|
|
4769
5025
|
const fd = openSync5(path2, constants5.O_RDONLY | constants5.O_DIRECTORY);
|
|
@@ -4774,13 +5030,13 @@ class HistoryFiles {
|
|
|
4774
5030
|
}
|
|
4775
5031
|
if (path2 === this.coordination.directory)
|
|
4776
5032
|
break;
|
|
4777
|
-
if (path2 ===
|
|
5033
|
+
if (path2 === dirname7(path2))
|
|
4778
5034
|
corrupt();
|
|
4779
5035
|
}
|
|
4780
5036
|
}
|
|
4781
5037
|
assertReadFormat(budget) {
|
|
4782
5038
|
budget.reserve(this.formatSnapshot.bytes.length);
|
|
4783
|
-
this.files.assert(
|
|
5039
|
+
this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
|
|
4784
5040
|
}
|
|
4785
5041
|
assertPublished(segment, end, seq) {
|
|
4786
5042
|
const fence = publicationFences.get(this.directory);
|
|
@@ -4818,7 +5074,7 @@ class HistoryFiles {
|
|
|
4818
5074
|
const dir = opendirSync2(path2);
|
|
4819
5075
|
try {
|
|
4820
5076
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
4821
|
-
const p =
|
|
5077
|
+
const p = join8(path2, e.name), st = lstatSync6(p);
|
|
4822
5078
|
if (st.isDirectory())
|
|
4823
5079
|
bytes += walk(p, depth + 1);
|
|
4824
5080
|
else {
|
|
@@ -4853,7 +5109,7 @@ class HistoryFiles {
|
|
|
4853
5109
|
state.bytes = 0;
|
|
4854
5110
|
state.identity = null;
|
|
4855
5111
|
let count = 0;
|
|
4856
|
-
const ledger =
|
|
5112
|
+
const ledger = join8(this.directory, "ledger");
|
|
4857
5113
|
if (!this.files.directory(ledger))
|
|
4858
5114
|
corrupt();
|
|
4859
5115
|
const dir = opendirSync2(ledger);
|
|
@@ -4901,9 +5157,9 @@ class HistoryFiles {
|
|
|
4901
5157
|
const line = file.bytes.subarray(at, end + 1);
|
|
4902
5158
|
if (line.length > RECORD_BYTES)
|
|
4903
5159
|
corrupt();
|
|
4904
|
-
let
|
|
5160
|
+
let decoded2;
|
|
4905
5161
|
try {
|
|
4906
|
-
|
|
5162
|
+
decoded2 = decodeJournalText(new TextDecoder("utf-8", { fatal: true }).decode(line));
|
|
4907
5163
|
} catch {
|
|
4908
5164
|
corrupt();
|
|
4909
5165
|
}
|
|
@@ -4911,29 +5167,29 @@ class HistoryFiles {
|
|
|
4911
5167
|
state.records++;
|
|
4912
5168
|
if (state.records > LIMITS.segmentRecords)
|
|
4913
5169
|
corrupt();
|
|
4914
|
-
if (
|
|
5170
|
+
if (decoded2.kind === "record") {
|
|
4915
5171
|
let base = state.committedSeq;
|
|
4916
|
-
if (
|
|
4917
|
-
if (pending.length || at !== line.length || n !==
|
|
5172
|
+
if (decoded2.value.kind === "recovery") {
|
|
5173
|
+
if (pending.length || at !== line.length || n !== decoded2.value.previous.segment + 1)
|
|
4918
5174
|
corrupt();
|
|
4919
|
-
if (
|
|
5175
|
+
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
5176
|
corrupt();
|
|
4921
|
-
base =
|
|
5177
|
+
base = decoded2.value.previous.highSeq;
|
|
4922
5178
|
} else if (preceding.tail && state.committedBytes === 0)
|
|
4923
5179
|
corrupt();
|
|
4924
5180
|
const next = BigInt(base) + BigInt(pending.length) + 1n;
|
|
4925
|
-
if (
|
|
5181
|
+
if (decoded2.seq !== String(next))
|
|
4926
5182
|
corrupt();
|
|
4927
|
-
state.highSeq =
|
|
5183
|
+
state.highSeq = decoded2.seq;
|
|
4928
5184
|
pendingBytes += line.length;
|
|
4929
5185
|
if (pendingBytes > 2 * RECORD_BYTES)
|
|
4930
5186
|
corrupt();
|
|
4931
|
-
pending.push(
|
|
5187
|
+
pending.push(decoded2);
|
|
4932
5188
|
checksum.update(line);
|
|
4933
5189
|
continue;
|
|
4934
5190
|
}
|
|
4935
5191
|
const artifacts = batchArtifacts(pending);
|
|
4936
|
-
if (!pending.length || !validBatchSequence(pending,
|
|
5192
|
+
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
5193
|
corrupt();
|
|
4938
5194
|
if (verifyArtifacts)
|
|
4939
5195
|
for (const artifact of artifacts)
|
|
@@ -4946,12 +5202,12 @@ class HistoryFiles {
|
|
|
4946
5202
|
if (e.kind === "owner" && e.value.kind === "fact" && e.value.fact.kind === "parent-admitted")
|
|
4947
5203
|
this.readAdmission(e.value.fact.runId, e.value.fact.identity, budget);
|
|
4948
5204
|
}
|
|
4949
|
-
state.committedSeq =
|
|
5205
|
+
state.committedSeq = decoded2.last;
|
|
4950
5206
|
state.committedBytes = at;
|
|
4951
|
-
state.navigationRoot =
|
|
5207
|
+
state.navigationRoot = decoded2.navigationRoot;
|
|
4952
5208
|
yield {
|
|
4953
5209
|
rows: pending,
|
|
4954
|
-
marker:
|
|
5210
|
+
marker: decoded2,
|
|
4955
5211
|
segment: n,
|
|
4956
5212
|
identity: file.identity,
|
|
4957
5213
|
segmentBytes: file.bytes.length
|
|
@@ -5018,7 +5274,7 @@ class HistoryFiles {
|
|
|
5018
5274
|
let committed;
|
|
5019
5275
|
const fd = await open(path2, constants5.O_WRONLY | constants5.O_APPEND | (constants5.O_NOFOLLOW ?? 0));
|
|
5020
5276
|
const assertOriginal = (bytes) => {
|
|
5021
|
-
const original = fstatSync4(fd.fd), selected2 =
|
|
5277
|
+
const original = fstatSync4(fd.fd), selected2 = lstatSync6(path2);
|
|
5022
5278
|
if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
|
|
5023
5279
|
corrupt();
|
|
5024
5280
|
const snapshot = this.files.read(path2, LIMITS.segmentBytes);
|
|
@@ -5135,7 +5391,7 @@ class HistoryFiles {
|
|
|
5135
5391
|
try {
|
|
5136
5392
|
lease.assert();
|
|
5137
5393
|
this.files.assert(path2, before, LIMITS.segmentBytes);
|
|
5138
|
-
const opened = await fd.stat(), selected2 =
|
|
5394
|
+
const opened = await fd.stat(), selected2 = lstatSync6(path2);
|
|
5139
5395
|
if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
|
|
5140
5396
|
corrupt();
|
|
5141
5397
|
beforeEffect?.();
|
|
@@ -5170,7 +5426,7 @@ class HistoryFiles {
|
|
|
5170
5426
|
checkpoint(lease, state) {
|
|
5171
5427
|
lease.assert();
|
|
5172
5428
|
this.assertWritable();
|
|
5173
|
-
const path2 =
|
|
5429
|
+
const path2 = join8(this.directory, "checkpoint.json"), old = this.files.read(path2);
|
|
5174
5430
|
const body = encoded({
|
|
5175
5431
|
schema: "cadence-history-checkpoint/2",
|
|
5176
5432
|
storeId: this.format.storeId,
|
|
@@ -5583,7 +5839,7 @@ function digestHistory(files, exclude) {
|
|
|
5583
5839
|
const walk = (path2, depth) => {
|
|
5584
5840
|
if (depth > 8 || !files.files.directory(path2))
|
|
5585
5841
|
throw new ContractError("corrupt-selected");
|
|
5586
|
-
const directory2 =
|
|
5842
|
+
const directory2 = lstatSync7(path2);
|
|
5587
5843
|
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
5588
5844
|
`);
|
|
5589
5845
|
const dir = opendirSync3(path2), names2 = [];
|
|
@@ -5597,11 +5853,11 @@ function digestHistory(files, exclude) {
|
|
|
5597
5853
|
dir.closeSync();
|
|
5598
5854
|
}
|
|
5599
5855
|
for (const name of names2.sort()) {
|
|
5600
|
-
const selected2 =
|
|
5856
|
+
const selected2 = join9(path2, name);
|
|
5601
5857
|
if (selected2 === exclude)
|
|
5602
5858
|
continue;
|
|
5603
5859
|
const rel = relative4(files.directory, selected2);
|
|
5604
|
-
if (
|
|
5860
|
+
if (lstatSync7(selected2).isDirectory()) {
|
|
5605
5861
|
digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
|
|
5606
5862
|
`);
|
|
5607
5863
|
walk(selected2, depth + 1);
|
|
@@ -5908,7 +6164,7 @@ async function localRecovery(c, operation) {
|
|
|
5908
6164
|
|
|
5909
6165
|
// src/service/runtime.ts
|
|
5910
6166
|
import { homedir as homedir4 } from "node:os";
|
|
5911
|
-
import { dirname as
|
|
6167
|
+
import { dirname as dirname8, isAbsolute as isAbsolute6, join as join10, resolve as resolve4 } from "node:path";
|
|
5912
6168
|
|
|
5913
6169
|
// src/service/config.ts
|
|
5914
6170
|
import { isIP } from "node:net";
|
|
@@ -5978,10 +6234,11 @@ function decodeHostConfig(value) {
|
|
|
5978
6234
|
const tokenSource = oneOf(o.tokenSource, [
|
|
5979
6235
|
"generated",
|
|
5980
6236
|
"environment",
|
|
5981
|
-
"file"
|
|
6237
|
+
"file",
|
|
6238
|
+
"config"
|
|
5982
6239
|
]);
|
|
5983
6240
|
const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
|
|
5984
|
-
if (
|
|
6241
|
+
if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
|
|
5985
6242
|
throw new ContractError("invalid");
|
|
5986
6243
|
return {
|
|
5987
6244
|
schema: literal(o.schema, "cadence-service-host/1"),
|
|
@@ -6042,13 +6299,13 @@ function selectStart(options, ambient) {
|
|
|
6042
6299
|
throw new ContractError("invalid");
|
|
6043
6300
|
let fromFile;
|
|
6044
6301
|
if (options.config) {
|
|
6045
|
-
const path2 = resolve4(options.config), f = new PrivateFiles(
|
|
6302
|
+
const path2 = resolve4(options.config), f = new PrivateFiles(dirname8(path2));
|
|
6046
6303
|
const bytes = f.read(path2);
|
|
6047
6304
|
if (!bytes)
|
|
6048
6305
|
throw new ContractError("corrupt-selected");
|
|
6049
6306
|
fromFile = decodeHostConfig(json(bytes));
|
|
6050
6307
|
}
|
|
6051
|
-
const
|
|
6308
|
+
const base = old ?? fromFile ?? decodeHostConfig({
|
|
6052
6309
|
schema: "cadence-service-host/1",
|
|
6053
6310
|
cwd: workspace.cwd,
|
|
6054
6311
|
http: {
|
|
@@ -6057,10 +6314,23 @@ function selectStart(options, ambient) {
|
|
|
6057
6314
|
port: options.port ?? 17423,
|
|
6058
6315
|
proxy: null
|
|
6059
6316
|
},
|
|
6060
|
-
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ??
|
|
6317
|
+
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join10(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
|
|
6061
6318
|
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
6062
6319
|
tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
|
|
6063
6320
|
});
|
|
6321
|
+
const server = options.serverConfig !== undefined ? resolve4(options.serverConfig) : old?.tokenSource === "config" ? old.tokenFile : null;
|
|
6322
|
+
let selected2 = base;
|
|
6323
|
+
if (server) {
|
|
6324
|
+
if (options.tokenFile !== undefined)
|
|
6325
|
+
throw new ContractError("conflict");
|
|
6326
|
+
const { port } = readServerConfig(server).config;
|
|
6327
|
+
selected2 = decodeHostConfig({
|
|
6328
|
+
...base,
|
|
6329
|
+
http: { ...base.http, port },
|
|
6330
|
+
tokenSource: "config",
|
|
6331
|
+
tokenFile: server
|
|
6332
|
+
});
|
|
6333
|
+
}
|
|
6064
6334
|
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
6335
|
throw new ContractError("conflict");
|
|
6066
6336
|
return {
|
|
@@ -6211,6 +6481,11 @@ async function stopService(c) {
|
|
|
6211
6481
|
function failedStatus(result) {
|
|
6212
6482
|
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
|
|
6213
6483
|
}
|
|
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.
|
|
6487
|
+
`;
|
|
6488
|
+
}
|
|
6214
6489
|
function startupGuidance(result, input) {
|
|
6215
6490
|
let report;
|
|
6216
6491
|
try {
|
|
@@ -6229,10 +6504,10 @@ function startupGuidance(result, input) {
|
|
|
6229
6504
|
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
6230
6505
|
`;
|
|
6231
6506
|
case "pi-auth":
|
|
6232
|
-
return `${
|
|
6507
|
+
return `${join11(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
6233
6508
|
`;
|
|
6234
6509
|
case "pi-models":
|
|
6235
|
-
return `${
|
|
6510
|
+
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
6511
|
`;
|
|
6237
6512
|
case "pi-runtime":
|
|
6238
6513
|
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 +6520,26 @@ ${inspect}` : "";
|
|
|
6245
6520
|
async function startService(options, foreground, hostUrl) {
|
|
6246
6521
|
const network = networkSnapshot(process.env);
|
|
6247
6522
|
const env = hostEnvironment(process.env);
|
|
6248
|
-
|
|
6523
|
+
let input;
|
|
6524
|
+
try {
|
|
6525
|
+
input = decodeHostInput(selectStart(options, process.env));
|
|
6526
|
+
} catch (error) {
|
|
6527
|
+
if (error instanceof ServerConfigRefusal)
|
|
6528
|
+
process.stderr.write(serverConfigHint(error));
|
|
6529
|
+
throw error;
|
|
6530
|
+
}
|
|
6249
6531
|
const serialized = `${JSON.stringify(input)}
|
|
6250
6532
|
`;
|
|
6251
6533
|
if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
|
|
6252
6534
|
throw new ContractError("oversize");
|
|
6253
6535
|
const c = new ServiceCoordination(input.workspace, input.stateHome);
|
|
6254
6536
|
const existing = await serviceStatus(c);
|
|
6255
|
-
if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive")
|
|
6537
|
+
if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
|
|
6538
|
+
const running = readHostConfig(c);
|
|
6539
|
+
if (running && encoded(running) !== encoded(input.config))
|
|
6540
|
+
throw new ContractError("conflict");
|
|
6256
6541
|
return existing;
|
|
6542
|
+
}
|
|
6257
6543
|
if (!sameNetwork(network, networkSnapshot(process.env)))
|
|
6258
6544
|
throw new ContractError("stale");
|
|
6259
6545
|
const child = spawn(process.execPath, [
|
|
@@ -6267,9 +6553,9 @@ async function startService(options, foreground, hostUrl) {
|
|
|
6267
6553
|
stdio: ["pipe", "pipe", "ignore"]
|
|
6268
6554
|
});
|
|
6269
6555
|
let completed = false;
|
|
6270
|
-
const closure = new Promise((resolve6) => child.once("close", (
|
|
6556
|
+
const closure = new Promise((resolve6) => child.once("close", (code2, signal) => {
|
|
6271
6557
|
completed = true;
|
|
6272
|
-
if (foreground && (
|
|
6558
|
+
if (foreground && (code2 !== 0 || signal !== null))
|
|
6273
6559
|
process.exitCode = 1;
|
|
6274
6560
|
resolve6();
|
|
6275
6561
|
}));
|
|
@@ -6358,6 +6644,14 @@ async function observeStarted(workspace) {
|
|
|
6358
6644
|
}
|
|
6359
6645
|
return { service };
|
|
6360
6646
|
}
|
|
6647
|
+
async function privateLink(workspace) {
|
|
6648
|
+
try {
|
|
6649
|
+
const value = closed(await nativeCall(new ServiceCoordination(workspace), "token-link", true), ["link"]);
|
|
6650
|
+
return text(value.link, 4096, true);
|
|
6651
|
+
} catch {
|
|
6652
|
+
return;
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6361
6655
|
var ownerControl = {
|
|
6362
6656
|
async ownerStatus(workspace) {
|
|
6363
6657
|
try {
|
|
@@ -6406,11 +6700,11 @@ async function serverAction(s, action, requested, linger) {
|
|
|
6406
6700
|
return { status: "unsupported" };
|
|
6407
6701
|
}
|
|
6408
6702
|
}
|
|
6409
|
-
function serverGuidance(s, action, result) {
|
|
6410
|
-
const cli = piUserInstall(s) ? `node ${JSON.stringify(
|
|
6703
|
+
function serverGuidance(s, action, result, link) {
|
|
6704
|
+
const cli = piUserInstall(s) ? `node ${JSON.stringify(join11(s.root, "src", "service-cli.mjs"))}` : "cadence";
|
|
6411
6705
|
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
6412
6706
|
` : "";
|
|
6413
|
-
return note + serverStatusGuidance(s, action, result, cli);
|
|
6707
|
+
return note + serverStatusGuidance(s, action, result, cli, link);
|
|
6414
6708
|
}
|
|
6415
6709
|
function lingerGuidance(linger) {
|
|
6416
6710
|
switch (linger) {
|
|
@@ -6437,10 +6731,11 @@ function managerDetail(s) {
|
|
|
6437
6731
|
return `run its launcher to see why: wscript ${JSON.stringify(launcherPath(s))}`;
|
|
6438
6732
|
}
|
|
6439
6733
|
}
|
|
6440
|
-
function serverStatusGuidance(s, action, result, cli) {
|
|
6734
|
+
function serverStatusGuidance(s, action, result, cli, link) {
|
|
6441
6735
|
const workspace = JSON.stringify(result.workspace ?? "");
|
|
6442
6736
|
const manage = `Manage: ${cli} server status|restart|stop|uninstall`;
|
|
6443
6737
|
const when = s.platform === "linux" ? "at boot or login" : "at login";
|
|
6738
|
+
const config = `Port and token: ${PORT_KEY} and ${TOKEN_KEY} in ${serverConfigPath(s.home)} (edit them, then: ${cli} server restart)`;
|
|
6444
6739
|
switch (result.status) {
|
|
6445
6740
|
case "started": {
|
|
6446
6741
|
const linger = lingerGuidance(result.linger);
|
|
@@ -6449,14 +6744,22 @@ function serverStatusGuidance(s, action, result, cli) {
|
|
|
6449
6744
|
Inspect: ${cli} diagnose --workspace ${workspace}
|
|
6450
6745
|
${manage}
|
|
6451
6746
|
${linger}`;
|
|
6452
|
-
|
|
6747
|
+
if (!link)
|
|
6748
|
+
return `Cadence WebUI: ${result.endpoint} (workspace ${result.workspace}; starts ${when})
|
|
6453
6749
|
Enter with your private link: ${cli} token-link --workspace ${workspace} --reveal
|
|
6750
|
+
${config}
|
|
6751
|
+
${manage}
|
|
6752
|
+
${linger}`;
|
|
6753
|
+
return `Cadence WebUI: ${link}
|
|
6754
|
+
Open this private link (workspace ${result.workspace}; starts ${when}); its token admits anyone who has it.
|
|
6755
|
+
${config}
|
|
6454
6756
|
${manage}
|
|
6455
6757
|
${linger}`;
|
|
6456
6758
|
}
|
|
6457
6759
|
case "waiting-for-pi-login":
|
|
6458
|
-
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${
|
|
6760
|
+
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join11(agentDirectory(s), "auth.json")}.
|
|
6459
6761
|
Log in to Pi, then: ${cli} server restart
|
|
6762
|
+
${config}
|
|
6460
6763
|
${lingerGuidance(result.linger)}`;
|
|
6461
6764
|
case "stopped":
|
|
6462
6765
|
return action === "stop" ? `Cadence stopped; it starts again ${when}, or now with: ${cli} server restart
|
|
@@ -6531,9 +6834,25 @@ async function serverMain(argv, hostUrl) {
|
|
|
6531
6834
|
}
|
|
6532
6835
|
if (argv.length)
|
|
6533
6836
|
throw new ContractError("invalid");
|
|
6534
|
-
const s = hostSystem(
|
|
6837
|
+
const s = hostSystem(dirname9(dirname9(fileURLToPath(hostUrl))), ownerControl);
|
|
6535
6838
|
if (action === "install")
|
|
6536
6839
|
process.stderr.write(researchTemplate(s.home));
|
|
6840
|
+
if (action === "install" || action === "restart") {
|
|
6841
|
+
try {
|
|
6842
|
+
const { added, exposed } = ensureServerConfig(s.home);
|
|
6843
|
+
const path2 = serverConfigPath(s.home);
|
|
6844
|
+
if (added.length)
|
|
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.
|
|
6849
|
+
`);
|
|
6850
|
+
} catch (error) {
|
|
6851
|
+
if (error instanceof ServerConfigRefusal)
|
|
6852
|
+
process.stderr.write(serverConfigHint(error));
|
|
6853
|
+
throw error;
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6537
6856
|
let result;
|
|
6538
6857
|
try {
|
|
6539
6858
|
result = await serverAction(s, action, requested, linger);
|
|
@@ -6543,9 +6862,13 @@ async function serverMain(argv, hostUrl) {
|
|
|
6543
6862
|
`);
|
|
6544
6863
|
throw error;
|
|
6545
6864
|
}
|
|
6546
|
-
|
|
6865
|
+
let link;
|
|
6866
|
+
if (result.status === "started" && result.workspace) {
|
|
6547
6867
|
Object.assign(result, await observeStarted(result.workspace));
|
|
6548
|
-
|
|
6868
|
+
if (result.service === "alive")
|
|
6869
|
+
link = await privateLink(result.workspace);
|
|
6870
|
+
}
|
|
6871
|
+
process.stderr.write(serverGuidance(s, action, result, link));
|
|
6549
6872
|
process.stdout.write(`${JSON.stringify(result)}
|
|
6550
6873
|
`);
|
|
6551
6874
|
if ([
|
|
@@ -6567,7 +6890,7 @@ async function managementMain(argv, hostUrl) {
|
|
|
6567
6890
|
if (command === "--help" && !argv.length) {
|
|
6568
6891
|
process.stdout.write(`cadence start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
6569
6892
|
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.
|
|
6893
|
+
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
6894
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
6572
6895
|
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
6896
|
`);
|
|
@@ -6627,6 +6950,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
6627
6950
|
options.agentDir = value;
|
|
6628
6951
|
else if (flag === "--token-file")
|
|
6629
6952
|
options.tokenFile = value;
|
|
6953
|
+
else if (flag === "--server-config")
|
|
6954
|
+
options.serverConfig = value;
|
|
6630
6955
|
else if (flag === "--port")
|
|
6631
6956
|
options.port = portValue(value);
|
|
6632
6957
|
else if (flag === "--host" && (value === "127.0.0.1" || value === "::1"))
|