@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.
- package/README.md +27 -4
- package/docs/remote-access.md +3 -2
- package/docs/service-management.md +32 -5
- package/package.json +1 -1
- package/src/service/coordination.ts +25 -2
- package/src/service-cli.mjs +713 -479
- package/src/service-host.mjs +272 -78
- 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,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
|
|
2515
|
-
mkdirSync as
|
|
2514
|
+
lstatSync as lstatSync3,
|
|
2515
|
+
mkdirSync as mkdirSync4,
|
|
2516
2516
|
readFileSync,
|
|
2517
2517
|
realpathSync as realpathSync2,
|
|
2518
|
-
renameSync as
|
|
2518
|
+
renameSync as renameSync3,
|
|
2519
2519
|
rmSync,
|
|
2520
2520
|
statSync,
|
|
2521
|
-
writeFileSync as
|
|
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
|
|
2527
|
-
isAbsolute as
|
|
2528
|
-
join as
|
|
2529
|
-
resolve as
|
|
2530
|
-
sep as
|
|
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
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
function
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
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
|
|
2889
|
-
mkdirSync as
|
|
2577
|
+
lstatSync as lstatSync2,
|
|
2578
|
+
mkdirSync as mkdirSync3,
|
|
2890
2579
|
openSync as openSync3,
|
|
2891
2580
|
readSync as readSync2,
|
|
2892
|
-
renameSync as
|
|
2581
|
+
renameSync as renameSync2,
|
|
2893
2582
|
unlinkSync as unlinkSync2,
|
|
2894
|
-
writeFileSync as
|
|
2583
|
+
writeFileSync as writeFileSync2
|
|
2895
2584
|
} from "node:fs";
|
|
2896
|
-
import { dirname as
|
|
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 =
|
|
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 (
|
|
2662
|
+
if (isAbsolute2(tail) || tail === ".." || tail.startsWith(`..${sep2}`))
|
|
2974
2663
|
refused();
|
|
2975
2664
|
if (!create) {
|
|
2976
|
-
const full =
|
|
2665
|
+
const full = resolve2(path2);
|
|
2977
2666
|
if (walks?.reuse(full, this.ownerOnly))
|
|
2978
2667
|
return true;
|
|
2979
|
-
const 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(
|
|
2683
|
+
privateMode(lstatSync2(this.namespace), this.ownerOnly);
|
|
2995
2684
|
let at = this.namespace;
|
|
2996
|
-
for (const part of tail.split(
|
|
2997
|
-
at =
|
|
2685
|
+
for (const part of tail.split(sep2).filter(Boolean)) {
|
|
2686
|
+
at = join2(at, part);
|
|
2998
2687
|
let st;
|
|
2999
2688
|
try {
|
|
3000
|
-
st =
|
|
2689
|
+
st = lstatSync2(at);
|
|
3001
2690
|
} catch (error) {
|
|
3002
2691
|
if (!missing(error))
|
|
3003
2692
|
throw error;
|
|
3004
2693
|
try {
|
|
3005
|
-
|
|
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 =
|
|
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(
|
|
2708
|
+
if (!this.directory(dirname2(path2), false, walks))
|
|
3020
2709
|
return;
|
|
3021
2710
|
let before;
|
|
3022
2711
|
try {
|
|
3023
|
-
before =
|
|
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,
|
|
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(
|
|
2750
|
+
this.directory(dirname2(path2), true);
|
|
3062
2751
|
const fd = retryWindowsFileOperation(() => openSync3(path2, "wx", 384));
|
|
3063
2752
|
try {
|
|
3064
|
-
|
|
2753
|
+
writeFileSync2(fd, bytes);
|
|
3065
2754
|
fsyncSync2(fd);
|
|
3066
2755
|
} finally {
|
|
3067
2756
|
closeSync3(fd);
|
|
3068
2757
|
}
|
|
3069
|
-
this.syncDirectory(
|
|
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 =
|
|
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(() =>
|
|
3081
|
-
this.syncDirectory(
|
|
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(
|
|
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
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
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
|
|
3090
|
+
function readGenerated(path2, marker) {
|
|
3116
3091
|
try {
|
|
3117
|
-
|
|
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
|
|
3110
|
+
return false;
|
|
3120
3111
|
}
|
|
3121
3112
|
}
|
|
3122
|
-
function
|
|
3123
|
-
|
|
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 =
|
|
3401
|
+
const namespace = dirname5(this.store.privateRoot);
|
|
3254
3402
|
this.files = new PrivateFiles(namespace);
|
|
3255
|
-
this.stateHome =
|
|
3256
|
-
this.directory =
|
|
3257
|
-
this.registryDirectory =
|
|
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
|
|
3408
|
+
return join5(this.directory, name);
|
|
3261
3409
|
}
|
|
3262
3410
|
registryPath(name) {
|
|
3263
|
-
return
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
3897
|
+
return join6(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
3739
3898
|
}
|
|
3740
3899
|
var xml = (value) => checked(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
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
|
-
...[
|
|
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:
|
|
3977
|
+
cli: join6(s.root, "src", "service-cli.mjs"),
|
|
3810
3978
|
workspace,
|
|
3979
|
+
serverConfig: serverConfigPath(s.home),
|
|
3811
3980
|
environment: serviceEnvironment(s),
|
|
3812
|
-
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(
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
4762
|
+
this.directory = join8(coordination.directory, "history-v2");
|
|
4593
4763
|
this.files = coordination.files;
|
|
4594
4764
|
}
|
|
4595
4765
|
static open(coordination) {
|
|
4596
|
-
const dir =
|
|
4766
|
+
const dir = join8(coordination.directory, "history-v2");
|
|
4597
4767
|
if (!coordination.files.directory(dir))
|
|
4598
4768
|
return;
|
|
4599
|
-
const file = coordination.files.read(
|
|
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 =
|
|
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(
|
|
4795
|
+
c.files.directory(join8(dir, name), true);
|
|
4626
4796
|
}
|
|
4627
4797
|
lease.assert();
|
|
4628
|
-
const keyPath =
|
|
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:
|
|
4802
|
+
key: randomBytes3(32).toString("base64url")
|
|
4633
4803
|
}));
|
|
4634
4804
|
lease.assert();
|
|
4635
|
-
const snapshot = c.files.create(
|
|
4805
|
+
const snapshot = c.files.create(join8(dir, "format.json"), encoded(format));
|
|
4636
4806
|
const result = new HistoryFiles(c, format, snapshot);
|
|
4637
|
-
result.syncParents(
|
|
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(
|
|
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(
|
|
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,
|
|
4879
|
+
assertIncarnation(this, join8(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
4710
4880
|
}
|
|
4711
4881
|
segmentPath(segment) {
|
|
4712
|
-
return
|
|
4882
|
+
return join8(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
4713
4883
|
}
|
|
4714
4884
|
artifactPath(ref) {
|
|
4715
|
-
return
|
|
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
|
|
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(
|
|
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 =
|
|
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 ===
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
5074
|
+
let decoded2;
|
|
4905
5075
|
try {
|
|
4906
|
-
|
|
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 (
|
|
5084
|
+
if (decoded2.kind === "record") {
|
|
4915
5085
|
let base = state.committedSeq;
|
|
4916
|
-
if (
|
|
4917
|
-
if (pending.length || at !== line.length || n !==
|
|
5086
|
+
if (decoded2.value.kind === "recovery") {
|
|
5087
|
+
if (pending.length || at !== line.length || n !== decoded2.value.previous.segment + 1)
|
|
4918
5088
|
corrupt();
|
|
4919
|
-
if (
|
|
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 =
|
|
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 (
|
|
5095
|
+
if (decoded2.seq !== String(next))
|
|
4926
5096
|
corrupt();
|
|
4927
|
-
state.highSeq =
|
|
5097
|
+
state.highSeq = decoded2.seq;
|
|
4928
5098
|
pendingBytes += line.length;
|
|
4929
5099
|
if (pendingBytes > 2 * RECORD_BYTES)
|
|
4930
5100
|
corrupt();
|
|
4931
|
-
pending.push(
|
|
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,
|
|
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 =
|
|
5119
|
+
state.committedSeq = decoded2.last;
|
|
4950
5120
|
state.committedBytes = at;
|
|
4951
|
-
state.navigationRoot =
|
|
5121
|
+
state.navigationRoot = decoded2.navigationRoot;
|
|
4952
5122
|
yield {
|
|
4953
5123
|
rows: pending,
|
|
4954
|
-
marker:
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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(
|
|
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
|
|
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 ??
|
|
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 `${
|
|
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 `${
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ${
|
|
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(
|
|
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
|
-
|
|
6774
|
+
let link;
|
|
6775
|
+
if (result.status === "started" && result.workspace) {
|
|
6547
6776
|
Object.assign(result, await observeStarted(result.workspace));
|
|
6548
|
-
|
|
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"))
|