@abelxiaoxing/cadence 2.0.10 → 2.0.11
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 +83 -25
- package/docs/reuse-and-distribution.md +3 -3
- package/docs/service-management.md +81 -3
- package/package.json +1 -1
- package/src/native-client.ts +15 -9
- package/src/operator-cli.mjs +13 -2
- package/src/sdk-worker.mjs +5 -1
- package/src/service-cli.mjs +465 -75
- package/src/service-host.mjs +45 -7
- package/src/task-store.ts +44 -10
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +6 -6
package/src/service-cli.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// src/service/cli.ts
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync, mkdirSync as
|
|
6
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
7
7
|
import { request } from "node:http";
|
|
8
|
-
import { dirname as dirname9, join as
|
|
8
|
+
import { dirname as dirname9, join as join12, resolve as resolve5 } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
// src/install-config.mjs
|
|
@@ -2225,15 +2225,19 @@ function readBounded(path2, max, packaged = false) {
|
|
|
2225
2225
|
}
|
|
2226
2226
|
}
|
|
2227
2227
|
function gitEnvironment() {
|
|
2228
|
+
const root = process.getuid?.() === 0;
|
|
2228
2229
|
return {
|
|
2229
|
-
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_"))),
|
|
2230
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_") && !(root && name === "SUDO_UID"))),
|
|
2230
2231
|
GIT_OPTIONAL_LOCKS: "0",
|
|
2231
2232
|
LC_ALL: "C"
|
|
2232
2233
|
};
|
|
2233
2234
|
}
|
|
2235
|
+
function gitArguments(args) {
|
|
2236
|
+
return process.platform !== "win32" && process.getuid?.() === 0 ? ["-c", "safe.directory=", "-c", "core.fsmonitor=false", ...args] : args;
|
|
2237
|
+
}
|
|
2234
2238
|
function resolveWorkspace(cwd) {
|
|
2235
2239
|
const actual = realpathSync(cwd);
|
|
2236
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2240
|
+
const result = spawnSync("git", gitArguments(["rev-parse", "--show-toplevel"]), {
|
|
2237
2241
|
cwd: actual,
|
|
2238
2242
|
env: gitEnvironment(),
|
|
2239
2243
|
encoding: "utf8",
|
|
@@ -2296,8 +2300,13 @@ function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
|
2296
2300
|
const canonical = realpathSync(root);
|
|
2297
2301
|
return contains(canonical, privateRootFor(canonical, stateHome));
|
|
2298
2302
|
}
|
|
2303
|
+
function privateWorkspace(requested, stateHome = defaultStateHome()) {
|
|
2304
|
+
return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
|
|
2305
|
+
}
|
|
2299
2306
|
var LANDING_WORKSPACE = "cadence";
|
|
2300
2307
|
function effectiveWorkspace(requested, stateHome, home = homedir2()) {
|
|
2308
|
+
if (privateWorkspace(requested, stateHome))
|
|
2309
|
+
throw new Error("Private state cannot be a workspace");
|
|
2301
2310
|
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
2302
2311
|
return { workspace: requested, redirected: false };
|
|
2303
2312
|
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
@@ -2309,6 +2318,8 @@ class TaskStore {
|
|
|
2309
2318
|
privateRoot;
|
|
2310
2319
|
constructor(root, stateHome = defaultStateHome()) {
|
|
2311
2320
|
this.root = realpathSync(root);
|
|
2321
|
+
if (privateWorkspace(this.root, stateHome))
|
|
2322
|
+
throw new Error("Private state cannot be a workspace");
|
|
2312
2323
|
this.tasks = join(this.root, ".cadence", "assistant", "tasks");
|
|
2313
2324
|
this.privateRoot = privateRootFor(this.root, stateHome);
|
|
2314
2325
|
if (contains(this.root, this.privateRoot))
|
|
@@ -2512,7 +2523,7 @@ var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
|
|
|
2512
2523
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2513
2524
|
import {
|
|
2514
2525
|
lstatSync as lstatSync4,
|
|
2515
|
-
mkdirSync as
|
|
2526
|
+
mkdirSync as mkdirSync5,
|
|
2516
2527
|
readFileSync,
|
|
2517
2528
|
realpathSync as realpathSync3,
|
|
2518
2529
|
renameSync as renameSync3,
|
|
@@ -2533,7 +2544,13 @@ import {
|
|
|
2533
2544
|
|
|
2534
2545
|
// src/service/server-config.ts
|
|
2535
2546
|
import { randomBytes } from "node:crypto";
|
|
2536
|
-
import {
|
|
2547
|
+
import {
|
|
2548
|
+
existsSync,
|
|
2549
|
+
lstatSync as lstatSync3,
|
|
2550
|
+
mkdirSync as mkdirSync4,
|
|
2551
|
+
realpathSync as realpathSync2,
|
|
2552
|
+
statSync
|
|
2553
|
+
} from "node:fs";
|
|
2537
2554
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2538
2555
|
|
|
2539
2556
|
// skills/_shared/load-config.mjs
|
|
@@ -2830,8 +2847,10 @@ function accessToken(value) {
|
|
|
2830
2847
|
throw new ContractError("invalid");
|
|
2831
2848
|
return result;
|
|
2832
2849
|
}
|
|
2833
|
-
function serverConfigPath(home) {
|
|
2834
|
-
|
|
2850
|
+
function serverConfigPath(home, name) {
|
|
2851
|
+
if (name !== undefined && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(name))
|
|
2852
|
+
throw new ContractError("invalid");
|
|
2853
|
+
return name === undefined ? join3(home, ".pi", "agent", "cadence", ".env") : join3(home, ".pi", "agent", "cadence", "servers", `${name}.env`);
|
|
2835
2854
|
}
|
|
2836
2855
|
|
|
2837
2856
|
class ServerConfigRefusal extends ContractError {
|
|
@@ -2974,19 +2993,33 @@ function readServerConfig(path2) {
|
|
|
2974
2993
|
return { ...file, config: decode(file.source) };
|
|
2975
2994
|
});
|
|
2976
2995
|
}
|
|
2977
|
-
function ensureServerConfig(home) {
|
|
2978
|
-
const path2 = serverConfigPath(home);
|
|
2996
|
+
function ensureServerConfig(home, options = {}) {
|
|
2997
|
+
const path2 = serverConfigPath(home, options.name);
|
|
2998
|
+
if (options.port !== undefined && (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535))
|
|
2999
|
+
throw new ContractError("invalid");
|
|
2979
3000
|
const added = refusing(path2, () => {
|
|
2980
3001
|
try {
|
|
2981
|
-
|
|
3002
|
+
if (options.name === undefined)
|
|
3003
|
+
ensureUserConfig(home);
|
|
3004
|
+
else if (!existsSync(path2)) {
|
|
3005
|
+
if (options.port === undefined)
|
|
3006
|
+
unusable("needs --port for a new named service");
|
|
3007
|
+
mkdirSync4(dirname3(path2), { recursive: true, mode: 448 });
|
|
3008
|
+
const directory = realpathSync2(dirname3(path2));
|
|
3009
|
+
new PrivateFiles(directory, false).create(join3(directory, `${options.name}.env`), "");
|
|
3010
|
+
}
|
|
2982
3011
|
} catch (error) {
|
|
3012
|
+
if (error instanceof Unusable)
|
|
3013
|
+
throw error;
|
|
2983
3014
|
unusable(`could not be created (${code(error)})`);
|
|
2984
3015
|
}
|
|
2985
3016
|
const file2 = load(path2);
|
|
2986
3017
|
const found = settings(file2.source);
|
|
2987
3018
|
const lines = [];
|
|
3019
|
+
if (found.port && options.port !== undefined && found.port.value !== String(options.port))
|
|
3020
|
+
unusable(`already selects another ${PORT_KEY}; edit that line explicitly, then restart`);
|
|
2988
3021
|
if (!found.port)
|
|
2989
|
-
lines.push(`${PORT_KEY}=${DEFAULT_PORT}`);
|
|
3022
|
+
lines.push(`${PORT_KEY}=${options.port ?? DEFAULT_PORT}`);
|
|
2990
3023
|
if (!found.token)
|
|
2991
3024
|
lines.push(`${TOKEN_KEY}=${randomBytes(32).toString("base64url")}`);
|
|
2992
3025
|
if (lines.length) {
|
|
@@ -3017,7 +3050,9 @@ function wscript(env) {
|
|
|
3017
3050
|
const root = env.SystemRoot;
|
|
3018
3051
|
return win32.join(root && win32.isAbsolute(root) ? root : "C:\\Windows", "System32", "wscript.exe");
|
|
3019
3052
|
}
|
|
3020
|
-
function hostSystem(root, owner) {
|
|
3053
|
+
function hostSystem(root, owner, selection = {}) {
|
|
3054
|
+
const scope = selection.scope ?? (process.platform === "linux" && process.getuid?.() === 0 ? "system" : "user");
|
|
3055
|
+
const unit = serviceUnit(selection.name);
|
|
3021
3056
|
const run = (command, args) => {
|
|
3022
3057
|
const r = spawnSync2(command, args, {
|
|
3023
3058
|
encoding: "utf8",
|
|
@@ -3032,9 +3067,10 @@ function hostSystem(root, owner) {
|
|
|
3032
3067
|
home: homedir3(),
|
|
3033
3068
|
node: process.execPath,
|
|
3034
3069
|
root,
|
|
3035
|
-
unit
|
|
3070
|
+
unit,
|
|
3071
|
+
scope,
|
|
3036
3072
|
uid: process.getuid?.(),
|
|
3037
|
-
systemctl: (args) => run("systemctl", ["--user", ...args]),
|
|
3073
|
+
systemctl: (args) => run("systemctl", [scope === "system" ? "--system" : "--user", ...args]),
|
|
3038
3074
|
linger: () => {
|
|
3039
3075
|
const r = run("loginctl", [
|
|
3040
3076
|
"show-user",
|
|
@@ -3074,6 +3110,24 @@ function hostSystem(root, owner) {
|
|
|
3074
3110
|
...owner
|
|
3075
3111
|
};
|
|
3076
3112
|
}
|
|
3113
|
+
function serviceUnit(name) {
|
|
3114
|
+
if (name === undefined)
|
|
3115
|
+
return UNIT;
|
|
3116
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(name))
|
|
3117
|
+
throw new ContractError("invalid");
|
|
3118
|
+
return `cadence-${name}.service`;
|
|
3119
|
+
}
|
|
3120
|
+
function serviceName(s) {
|
|
3121
|
+
if (s.unit === UNIT)
|
|
3122
|
+
return;
|
|
3123
|
+
const name = /^cadence-([A-Za-z0-9][A-Za-z0-9_-]{0,47})\.service$/.exec(s.unit)?.[1];
|
|
3124
|
+
if (!name)
|
|
3125
|
+
throw new ContractError("invalid");
|
|
3126
|
+
return name;
|
|
3127
|
+
}
|
|
3128
|
+
function serviceConfigPath(s) {
|
|
3129
|
+
return serverConfigPath(s.home, serviceName(s));
|
|
3130
|
+
}
|
|
3077
3131
|
function expandHome(value, home) {
|
|
3078
3132
|
if (value === "~")
|
|
3079
3133
|
return home;
|
|
@@ -3113,6 +3167,9 @@ function sdkPackages(root) {
|
|
|
3113
3167
|
return found;
|
|
3114
3168
|
}
|
|
3115
3169
|
function unitPath(s) {
|
|
3170
|
+
serviceName(s);
|
|
3171
|
+
if (s.scope === "system")
|
|
3172
|
+
return join4(s.unitDirectory ?? "/etc/systemd/system", s.unit);
|
|
3116
3173
|
const data = s.env.XDG_DATA_HOME;
|
|
3117
3174
|
return join4(data && isAbsolute3(data) ? data : join4(s.home, ".local", "share"), "systemd", "user", s.unit);
|
|
3118
3175
|
}
|
|
@@ -3122,6 +3179,8 @@ function servicePath(value) {
|
|
|
3122
3179
|
}
|
|
3123
3180
|
function serviceEnvironment(s) {
|
|
3124
3181
|
const out = [["PATH", servicePath(s.env.PATH)]];
|
|
3182
|
+
if (s.scope === "system")
|
|
3183
|
+
out.push(["HOME", s.home], ["USER", "root"], ["LOGNAME", "root"]);
|
|
3125
3184
|
for (const key of ["LANG", "LC_ALL"]) {
|
|
3126
3185
|
const value = s.env[key];
|
|
3127
3186
|
if (value)
|
|
@@ -3146,7 +3205,7 @@ function buildUnit(u) {
|
|
|
3146
3205
|
const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
|
|
3147
3206
|
return [
|
|
3148
3207
|
`${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`.",
|
|
3208
|
+
u.system ? "# System service (root); manage explicitly with cadence server <action> --system. No user manager or linger." : "# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
|
|
3150
3209
|
"[Unit]",
|
|
3151
3210
|
"Description=Cadence shared task service and WebUI",
|
|
3152
3211
|
"Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
|
|
@@ -3158,6 +3217,7 @@ function buildUnit(u) {
|
|
|
3158
3217
|
"",
|
|
3159
3218
|
"[Service]",
|
|
3160
3219
|
"Type=simple",
|
|
3220
|
+
...u.system ? ["User=root"] : [],
|
|
3161
3221
|
`WorkingDirectory=${specifiers(u.workspace)}`,
|
|
3162
3222
|
...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
|
|
3163
3223
|
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace, "--server-config", u.serverConfig])}`,
|
|
@@ -3168,7 +3228,7 @@ function buildUnit(u) {
|
|
|
3168
3228
|
"RestartSec=10",
|
|
3169
3229
|
"",
|
|
3170
3230
|
"[Install]",
|
|
3171
|
-
"WantedBy=default.target",
|
|
3231
|
+
u.system ? "WantedBy=multi-user.target" : "WantedBy=default.target",
|
|
3172
3232
|
""
|
|
3173
3233
|
].join(`
|
|
3174
3234
|
`);
|
|
@@ -3200,7 +3260,7 @@ function serviceStateHome(s) {
|
|
|
3200
3260
|
const state = s.platform === "win32" ? s.env.LOCALAPPDATA : s.env.XDG_STATE_HOME;
|
|
3201
3261
|
return state && isAbsolute3(state) ? state : join4(s.home, ...s.platform === "win32" ? ["AppData", "Local"] : [".local", "state"]);
|
|
3202
3262
|
}
|
|
3203
|
-
function selectWorkspace(s, requested, kept) {
|
|
3263
|
+
function selectWorkspace(s, requested, kept, create = true) {
|
|
3204
3264
|
let chosen;
|
|
3205
3265
|
if (requested) {
|
|
3206
3266
|
chosen = expandHome(requested, s.home);
|
|
@@ -3210,12 +3270,12 @@ function selectWorkspace(s, requested, kept) {
|
|
|
3210
3270
|
} else if (kept && directory(kept))
|
|
3211
3271
|
chosen = kept;
|
|
3212
3272
|
const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace: join4(s.home, LANDING_WORKSPACE), redirected: false };
|
|
3213
|
-
if (landing.workspace !== chosen)
|
|
3214
|
-
|
|
3273
|
+
if (create && landing.workspace !== chosen)
|
|
3274
|
+
mkdirSync5(landing.workspace, { recursive: true });
|
|
3215
3275
|
return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
|
|
3216
3276
|
}
|
|
3217
3277
|
function write(path2, content) {
|
|
3218
|
-
|
|
3278
|
+
mkdirSync5(dirname4(path2), { recursive: true });
|
|
3219
3279
|
const temporary = `${path2}.${process.pid}.tmp`;
|
|
3220
3280
|
try {
|
|
3221
3281
|
writeFileSync3(temporary, content, { mode: 420, flag: "wx" });
|
|
@@ -3255,7 +3315,7 @@ function bootStart(s, attempt) {
|
|
|
3255
3315
|
};
|
|
3256
3316
|
}
|
|
3257
3317
|
function configureAutostart(s, mode, requested, linger = false) {
|
|
3258
|
-
if (s.platform !== "linux")
|
|
3318
|
+
if (s.platform !== "linux" || s.scope === "system")
|
|
3259
3319
|
return { status: "unsupported" };
|
|
3260
3320
|
if (s.systemctl(["show-environment"]).status !== 0)
|
|
3261
3321
|
return { status: "unavailable" };
|
|
@@ -3274,15 +3334,19 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
3274
3334
|
node: s.node,
|
|
3275
3335
|
cli: join4(s.root, "src", "service-cli.mjs"),
|
|
3276
3336
|
workspace,
|
|
3277
|
-
serverConfig:
|
|
3337
|
+
serverConfig: serviceConfigPath(s),
|
|
3278
3338
|
agentDir,
|
|
3279
3339
|
sdk,
|
|
3280
3340
|
environment: serviceEnvironment(s)
|
|
3281
3341
|
});
|
|
3282
3342
|
const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
|
|
3283
3343
|
const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
|
|
3284
|
-
if (active)
|
|
3285
|
-
|
|
3344
|
+
if (active && s.systemctl(["stop", s.unit]).status !== 0)
|
|
3345
|
+
return {
|
|
3346
|
+
status: "unconfirmed",
|
|
3347
|
+
unit: path2,
|
|
3348
|
+
workspace: unitWorkspace(previous)
|
|
3349
|
+
};
|
|
3286
3350
|
write(path2, content);
|
|
3287
3351
|
s.systemctl(["daemon-reload"]);
|
|
3288
3352
|
const result = {
|
|
@@ -3302,7 +3366,7 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
3302
3366
|
return { status: started ? "started" : "failed", ...result, ...boot() };
|
|
3303
3367
|
}
|
|
3304
3368
|
function stopAutostart(s) {
|
|
3305
|
-
if (s.platform !== "linux")
|
|
3369
|
+
if (s.platform !== "linux" || s.scope === "system")
|
|
3306
3370
|
return { status: "unsupported" };
|
|
3307
3371
|
const path2 = unitPath(s);
|
|
3308
3372
|
const previous = readGenerated(path2, MARKER);
|
|
@@ -3319,7 +3383,7 @@ function stopAutostart(s) {
|
|
|
3319
3383
|
};
|
|
3320
3384
|
}
|
|
3321
3385
|
function removeAutostart(s) {
|
|
3322
|
-
if (s.platform !== "linux")
|
|
3386
|
+
if (s.platform !== "linux" || s.scope === "system")
|
|
3323
3387
|
return { status: "unsupported" };
|
|
3324
3388
|
const path2 = unitPath(s);
|
|
3325
3389
|
const previous = readGenerated(path2, MARKER);
|
|
@@ -3337,7 +3401,7 @@ function removeAutostart(s) {
|
|
|
3337
3401
|
return { status: "removed", unit: path2 };
|
|
3338
3402
|
}
|
|
3339
3403
|
function autostartStatus(s) {
|
|
3340
|
-
if (s.platform !== "linux")
|
|
3404
|
+
if (s.platform !== "linux" || s.scope === "system")
|
|
3341
3405
|
return { status: "unsupported" };
|
|
3342
3406
|
const path2 = unitPath(s);
|
|
3343
3407
|
const previous = readGenerated(path2, MARKER);
|
|
@@ -6380,6 +6444,212 @@ function decodeStartupReport(value) {
|
|
|
6380
6444
|
};
|
|
6381
6445
|
}
|
|
6382
6446
|
|
|
6447
|
+
// src/service/system-service.ts
|
|
6448
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync6, realpathSync as realpathSync5, rmSync as rmSync4 } from "node:fs";
|
|
6449
|
+
import { isAbsolute as isAbsolute7, join as join11 } from "node:path";
|
|
6450
|
+
var MARKER4 = "# Generated by @abelxiaoxing/cadence";
|
|
6451
|
+
function boundary(s) {
|
|
6452
|
+
if (s.platform !== "linux" || s.scope !== "system")
|
|
6453
|
+
return { status: "unsupported" };
|
|
6454
|
+
if (s.uid !== 0)
|
|
6455
|
+
return { status: "root-required", scope: "system" };
|
|
6456
|
+
}
|
|
6457
|
+
function legacyPath(s) {
|
|
6458
|
+
return unitPath({ ...s, scope: "user", unitDirectory: undefined });
|
|
6459
|
+
}
|
|
6460
|
+
function observed(s) {
|
|
6461
|
+
const path2 = unitPath(s);
|
|
6462
|
+
let previous = readGenerated(path2, MARKER4);
|
|
6463
|
+
if (typeof previous === "string" && (!isAbsolute7(unitWorkspace(previous) ?? "") || !previous.includes(`
|
|
6464
|
+
User=root
|
|
6465
|
+
`)))
|
|
6466
|
+
previous = null;
|
|
6467
|
+
const result = { unit: path2, scope: "system" };
|
|
6468
|
+
return { path: path2, previous, result };
|
|
6469
|
+
}
|
|
6470
|
+
function masked(s) {
|
|
6471
|
+
return s.systemctl(["is-enabled", s.unit]).stdout.trim().startsWith("masked");
|
|
6472
|
+
}
|
|
6473
|
+
function sameStateEnvironment(s, previous) {
|
|
6474
|
+
try {
|
|
6475
|
+
const values = new Map;
|
|
6476
|
+
for (const line of previous.split(`
|
|
6477
|
+
`)) {
|
|
6478
|
+
if (!line.startsWith("Environment="))
|
|
6479
|
+
continue;
|
|
6480
|
+
const value = JSON.parse(line.slice("Environment=".length));
|
|
6481
|
+
if (typeof value !== "string")
|
|
6482
|
+
return false;
|
|
6483
|
+
const at = value.indexOf("=");
|
|
6484
|
+
const key = value.slice(0, at);
|
|
6485
|
+
if (key !== "HOME" && key !== "XDG_STATE_HOME")
|
|
6486
|
+
continue;
|
|
6487
|
+
if (values.has(key))
|
|
6488
|
+
return false;
|
|
6489
|
+
values.set(key, value.slice(at + 1).replaceAll("%%", "%"));
|
|
6490
|
+
}
|
|
6491
|
+
const state = s.env.XDG_STATE_HOME;
|
|
6492
|
+
return values.get("HOME") === s.home && values.get("XDG_STATE_HOME") === (state && isAbsolute7(state) ? state : undefined);
|
|
6493
|
+
} catch {
|
|
6494
|
+
return false;
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
async function ownerBoundary(s, workspace, managed) {
|
|
6498
|
+
const status = await s.ownerStatus(workspace);
|
|
6499
|
+
if (managed) {
|
|
6500
|
+
if (status === "absent")
|
|
6501
|
+
return { status: "unavailable", workspace };
|
|
6502
|
+
} else if (status !== "absent" && status !== "stopped") {
|
|
6503
|
+
return {
|
|
6504
|
+
status: status === "alive" ? "project-running" : "unavailable",
|
|
6505
|
+
workspace
|
|
6506
|
+
};
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
async function configureSystemService(s, mode, requested) {
|
|
6510
|
+
const refused2 = boundary(s);
|
|
6511
|
+
if (refused2)
|
|
6512
|
+
return refused2;
|
|
6513
|
+
if (existsSync2(legacyPath(s)))
|
|
6514
|
+
return { status: "scope-conflict", unit: legacyPath(s), scope: "system" };
|
|
6515
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
6516
|
+
return { status: "unavailable", scope: "system" };
|
|
6517
|
+
const { path: path2, previous, result } = observed(s);
|
|
6518
|
+
if (masked(s))
|
|
6519
|
+
return { status: "masked", ...result };
|
|
6520
|
+
if (previous === null)
|
|
6521
|
+
return { status: "foreign", ...result };
|
|
6522
|
+
if (mode === "restart" && previous === undefined)
|
|
6523
|
+
return { status: "absent", scope: "system" };
|
|
6524
|
+
const sdk = sdkPackages(s.root);
|
|
6525
|
+
if (!sdk)
|
|
6526
|
+
return { status: "missing-pi-sdk", scope: "system" };
|
|
6527
|
+
if (previous && !existsSync2(unitWorkspace(previous)))
|
|
6528
|
+
return { status: "workspace-changed", ...result };
|
|
6529
|
+
const selected2 = selectWorkspace(s, requested, unitWorkspace(previous), false);
|
|
6530
|
+
const selectedReal = existsSync2(selected2.workspace) ? realpathSync5(selected2.workspace) : undefined;
|
|
6531
|
+
const agentDir = agentDirectory(s);
|
|
6532
|
+
const content = buildUnit({
|
|
6533
|
+
system: true,
|
|
6534
|
+
node: s.node,
|
|
6535
|
+
cli: join11(s.root, "src", "service-cli.mjs"),
|
|
6536
|
+
workspace: selected2.workspace,
|
|
6537
|
+
serverConfig: serviceConfigPath(s),
|
|
6538
|
+
agentDir,
|
|
6539
|
+
sdk,
|
|
6540
|
+
environment: serviceEnvironment(s)
|
|
6541
|
+
});
|
|
6542
|
+
if (previous && !sameStateEnvironment(s, previous))
|
|
6543
|
+
return { status: "environment-changed", ...result };
|
|
6544
|
+
const active = s.systemctl(["is-active", "--quiet", s.unit]);
|
|
6545
|
+
if (active.status !== 0 && active.status !== 3 && !(active.status === 4 && previous === undefined))
|
|
6546
|
+
return { status: "unavailable", ...result };
|
|
6547
|
+
if (active.status === 0 && previous === undefined)
|
|
6548
|
+
return { status: "foreign", ...result };
|
|
6549
|
+
if (selectedReal && (previous === undefined || selected2.workspace !== unitWorkspace(previous))) {
|
|
6550
|
+
const target2 = await ownerBoundary(s, selected2.workspace, false);
|
|
6551
|
+
if (target2)
|
|
6552
|
+
return { ...selected2, ...target2, ...result };
|
|
6553
|
+
}
|
|
6554
|
+
const workspace = unitWorkspace(previous);
|
|
6555
|
+
if (workspace) {
|
|
6556
|
+
const refused3 = await ownerBoundary(s, workspace, active.status === 0);
|
|
6557
|
+
if (refused3)
|
|
6558
|
+
return { ...refused3, ...result };
|
|
6559
|
+
}
|
|
6560
|
+
const unsettled = active.status === 0 ? await settle(s, workspace) : undefined;
|
|
6561
|
+
if (unsettled)
|
|
6562
|
+
return { ...unsettled, ...result };
|
|
6563
|
+
if (readGenerated(path2, MARKER4) !== previous)
|
|
6564
|
+
return { status: "foreign", ...result };
|
|
6565
|
+
if (selectedReal) {
|
|
6566
|
+
try {
|
|
6567
|
+
if (realpathSync5(selected2.workspace) !== selectedReal)
|
|
6568
|
+
return { status: "workspace-changed", ...result };
|
|
6569
|
+
} catch {
|
|
6570
|
+
return { status: "workspace-changed", ...result };
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
if (active.status === 0 && s.systemctl(["stop", s.unit]).status !== 0)
|
|
6574
|
+
return {
|
|
6575
|
+
status: "unconfirmed",
|
|
6576
|
+
workspace: unitWorkspace(previous),
|
|
6577
|
+
...result
|
|
6578
|
+
};
|
|
6579
|
+
mkdirSync6(selected2.workspace, { recursive: true });
|
|
6580
|
+
write(path2, content);
|
|
6581
|
+
if (s.systemctl(["daemon-reload"]).status !== 0)
|
|
6582
|
+
return { status: "failed", ...result, ...selected2 };
|
|
6583
|
+
if (mode === "install" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
6584
|
+
return { status: "failed", ...result, ...selected2 };
|
|
6585
|
+
if (!regularFile(join11(agentDir, "auth.json")))
|
|
6586
|
+
return { status: "waiting-for-pi-login", ...result, ...selected2 };
|
|
6587
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
6588
|
+
return {
|
|
6589
|
+
status: s.systemctl(["start", s.unit]).status === 0 ? "started" : "failed",
|
|
6590
|
+
...result,
|
|
6591
|
+
...selected2
|
|
6592
|
+
};
|
|
6593
|
+
}
|
|
6594
|
+
async function stopSystemService(s, remove = false) {
|
|
6595
|
+
const refused2 = boundary(s);
|
|
6596
|
+
if (refused2)
|
|
6597
|
+
return refused2;
|
|
6598
|
+
const { path: path2, previous, result } = observed(s);
|
|
6599
|
+
if (previous === null)
|
|
6600
|
+
return { status: masked(s) ? "masked" : "foreign", ...result };
|
|
6601
|
+
if (previous === undefined)
|
|
6602
|
+
return { status: "absent", scope: "system" };
|
|
6603
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
6604
|
+
return { status: "unavailable", ...result };
|
|
6605
|
+
const workspace = unitWorkspace(previous);
|
|
6606
|
+
if (!sameStateEnvironment(s, previous))
|
|
6607
|
+
return { status: "environment-changed", workspace, ...result };
|
|
6608
|
+
const active = s.systemctl(["is-active", "--quiet", s.unit]);
|
|
6609
|
+
if (active.status !== 0 && active.status !== 3)
|
|
6610
|
+
return { status: "unavailable", workspace, ...result };
|
|
6611
|
+
const owner = await ownerBoundary(s, workspace, active.status === 0);
|
|
6612
|
+
if (owner)
|
|
6613
|
+
return { ...owner, ...result };
|
|
6614
|
+
const unsettled = active.status === 0 ? await settle(s, workspace) : undefined;
|
|
6615
|
+
if (unsettled)
|
|
6616
|
+
return { ...unsettled, ...result };
|
|
6617
|
+
if (readGenerated(path2, MARKER4) !== previous)
|
|
6618
|
+
return { status: "foreign", ...result };
|
|
6619
|
+
if (active.status === 0 && s.systemctl(["stop", s.unit]).status !== 0)
|
|
6620
|
+
return { status: "unconfirmed", workspace, ...result };
|
|
6621
|
+
if (!remove)
|
|
6622
|
+
return { status: "stopped", workspace, ...result };
|
|
6623
|
+
if (s.systemctl(["disable", s.unit]).status !== 0)
|
|
6624
|
+
return { status: "failed", workspace, ...result };
|
|
6625
|
+
if (readGenerated(path2, MARKER4) !== previous)
|
|
6626
|
+
return { status: "foreign", ...result };
|
|
6627
|
+
rmSync4(path2);
|
|
6628
|
+
if (s.systemctl(["daemon-reload"]).status !== 0)
|
|
6629
|
+
return { status: "failed", ...result };
|
|
6630
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
6631
|
+
return { status: "removed", ...result };
|
|
6632
|
+
}
|
|
6633
|
+
function systemServiceStatus(s) {
|
|
6634
|
+
const refused2 = boundary(s);
|
|
6635
|
+
if (refused2)
|
|
6636
|
+
return refused2;
|
|
6637
|
+
const { previous, result } = observed(s);
|
|
6638
|
+
if (previous === undefined)
|
|
6639
|
+
return { status: "absent", scope: "system" };
|
|
6640
|
+
const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown";
|
|
6641
|
+
if (enabled.startsWith("masked"))
|
|
6642
|
+
return { status: "masked", enabled, ...result };
|
|
6643
|
+
if (previous === null)
|
|
6644
|
+
return { status: "foreign", ...result };
|
|
6645
|
+
return {
|
|
6646
|
+
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
6647
|
+
enabled,
|
|
6648
|
+
workspace: unitWorkspace(previous),
|
|
6649
|
+
...result
|
|
6650
|
+
};
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6383
6653
|
// src/service/cli.ts
|
|
6384
6654
|
function nativeCall(c, operation, reveal = false) {
|
|
6385
6655
|
const access = c.localAccess();
|
|
@@ -6444,14 +6714,14 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
6444
6714
|
}
|
|
6445
6715
|
var pause = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
6446
6716
|
async function serviceStatus(c, diagnose = false) {
|
|
6447
|
-
const
|
|
6448
|
-
if (
|
|
6449
|
-
return
|
|
6717
|
+
const observed2 = c.inspect();
|
|
6718
|
+
if (observed2.status === "absent" || observed2.status === "stopped")
|
|
6719
|
+
return observed2;
|
|
6450
6720
|
try {
|
|
6451
6721
|
return await nativeCall(c, diagnose ? "diagnose" : "status");
|
|
6452
6722
|
} catch {
|
|
6453
6723
|
return {
|
|
6454
|
-
status:
|
|
6724
|
+
status: observed2.status === "quarantined" ? "quarantined" : "unavailable"
|
|
6455
6725
|
};
|
|
6456
6726
|
}
|
|
6457
6727
|
}
|
|
@@ -6467,12 +6737,12 @@ async function stopService(c) {
|
|
|
6467
6737
|
}
|
|
6468
6738
|
const deadline = performance.now() + LIMITS.stopWaitMs;
|
|
6469
6739
|
while (performance.now() < deadline) {
|
|
6470
|
-
const
|
|
6471
|
-
if ("epoch" in
|
|
6740
|
+
const observed2 = c.inspect();
|
|
6741
|
+
if ("epoch" in observed2 && observed2.epoch === epoch && observed2.status === "stopped")
|
|
6472
6742
|
return { status: "stopped", epoch };
|
|
6473
|
-
if (
|
|
6743
|
+
if (observed2.status === "quarantined")
|
|
6474
6744
|
return { status: "quarantined", epoch };
|
|
6475
|
-
if ("epoch" in
|
|
6745
|
+
if ("epoch" in observed2 && observed2.epoch !== null && observed2.epoch !== epoch)
|
|
6476
6746
|
return { status: "unconfirmed", epoch };
|
|
6477
6747
|
await pause(25);
|
|
6478
6748
|
}
|
|
@@ -6504,10 +6774,10 @@ function startupGuidance(result, input) {
|
|
|
6504
6774
|
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
6505
6775
|
`;
|
|
6506
6776
|
case "pi-auth":
|
|
6507
|
-
return `${
|
|
6777
|
+
return `${join12(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
6508
6778
|
`;
|
|
6509
6779
|
case "pi-models":
|
|
6510
|
-
return `${
|
|
6780
|
+
return `${join12(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.
|
|
6511
6781
|
`;
|
|
6512
6782
|
case "pi-runtime":
|
|
6513
6783
|
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
@@ -6632,9 +6902,9 @@ async function observeStarted(workspace) {
|
|
|
6632
6902
|
while (performance.now() < deadline) {
|
|
6633
6903
|
try {
|
|
6634
6904
|
const c = new ServiceCoordination(workspace);
|
|
6635
|
-
const
|
|
6636
|
-
if (
|
|
6637
|
-
service = String(
|
|
6905
|
+
const observed2 = await serviceStatus(c);
|
|
6906
|
+
if (observed2 && typeof observed2 === "object" && "status" in observed2)
|
|
6907
|
+
service = String(observed2.status);
|
|
6638
6908
|
if (service === "alive")
|
|
6639
6909
|
return { service, endpoint: c.localAccess().endpoint };
|
|
6640
6910
|
if (service === "quarantined")
|
|
@@ -6644,9 +6914,9 @@ async function observeStarted(workspace) {
|
|
|
6644
6914
|
}
|
|
6645
6915
|
return { service };
|
|
6646
6916
|
}
|
|
6647
|
-
async function privateLink(workspace) {
|
|
6917
|
+
async function privateLink(workspace, stateHome) {
|
|
6648
6918
|
try {
|
|
6649
|
-
const value = closed(await nativeCall(new ServiceCoordination(workspace), "token-link", true), ["link"]);
|
|
6919
|
+
const value = closed(await nativeCall(new ServiceCoordination(workspace, stateHome), "token-link", true), ["link"]);
|
|
6650
6920
|
return text(value.link, 4096, true);
|
|
6651
6921
|
} catch {
|
|
6652
6922
|
return;
|
|
@@ -6655,8 +6925,8 @@ async function privateLink(workspace) {
|
|
|
6655
6925
|
var ownerControl = {
|
|
6656
6926
|
async ownerStatus(workspace) {
|
|
6657
6927
|
try {
|
|
6658
|
-
const
|
|
6659
|
-
return
|
|
6928
|
+
const observed2 = await serviceStatus(new ServiceCoordination(workspace));
|
|
6929
|
+
return observed2 && typeof observed2 === "object" && "status" in observed2 ? String(observed2.status) : "unavailable";
|
|
6660
6930
|
} catch {
|
|
6661
6931
|
return "unavailable";
|
|
6662
6932
|
}
|
|
@@ -6664,8 +6934,8 @@ var ownerControl = {
|
|
|
6664
6934
|
async stopOwner(workspace) {
|
|
6665
6935
|
try {
|
|
6666
6936
|
const c = new ServiceCoordination(workspace);
|
|
6667
|
-
const
|
|
6668
|
-
const status =
|
|
6937
|
+
const observed2 = await serviceStatus(c);
|
|
6938
|
+
const status = observed2 && typeof observed2 === "object" && "status" in observed2 ? String(observed2.status) : "unavailable";
|
|
6669
6939
|
if (status === "absent" || status === "stopped")
|
|
6670
6940
|
return "stopped";
|
|
6671
6941
|
return status === "alive" ? (await stopService(c)).status : status;
|
|
@@ -6679,6 +6949,13 @@ async function serverAction(s, action, requested, linger) {
|
|
|
6679
6949
|
const mode = action === "install" ? "install" : "restart";
|
|
6680
6950
|
switch (s.platform) {
|
|
6681
6951
|
case "linux":
|
|
6952
|
+
if (s.scope === "system") {
|
|
6953
|
+
if (install)
|
|
6954
|
+
return configureSystemService(s, mode, requested);
|
|
6955
|
+
if (action === "stop" || action === "uninstall")
|
|
6956
|
+
return stopSystemService(s, action === "uninstall");
|
|
6957
|
+
return systemServiceStatus(s);
|
|
6958
|
+
}
|
|
6682
6959
|
if (install)
|
|
6683
6960
|
return configureAutostart(s, mode, requested, linger);
|
|
6684
6961
|
if (action === "stop")
|
|
@@ -6701,7 +6978,7 @@ async function serverAction(s, action, requested, linger) {
|
|
|
6701
6978
|
}
|
|
6702
6979
|
}
|
|
6703
6980
|
function serverGuidance(s, action, result, link) {
|
|
6704
|
-
const cli = piUserInstall(s) ? `node ${JSON.stringify(
|
|
6981
|
+
const cli = piUserInstall(s) ? `node ${JSON.stringify(join12(s.root, "src", "service-cli.mjs"))}` : "cadence";
|
|
6705
6982
|
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
6706
6983
|
` : "";
|
|
6707
6984
|
return note + serverStatusGuidance(s, action, result, cli, link);
|
|
@@ -6724,7 +7001,7 @@ function lingerGuidance(linger) {
|
|
|
6724
7001
|
function managerDetail(s) {
|
|
6725
7002
|
switch (s.platform) {
|
|
6726
7003
|
case "linux":
|
|
6727
|
-
return
|
|
7004
|
+
return `see systemctl ${s.scope === "system" ? "--system" : "--user"} status ${s.unit} and journalctl ${s.scope === "system" ? "" : "--user "}-u ${s.unit}`;
|
|
6728
7005
|
case "darwin":
|
|
6729
7006
|
return `see launchctl print gui/${s.uid}/${LABEL} and ~/Library/Logs/cadence.log`;
|
|
6730
7007
|
default:
|
|
@@ -6733,9 +7010,10 @@ function managerDetail(s) {
|
|
|
6733
7010
|
}
|
|
6734
7011
|
function serverStatusGuidance(s, action, result, cli, link) {
|
|
6735
7012
|
const workspace = JSON.stringify(result.workspace ?? "");
|
|
6736
|
-
const
|
|
6737
|
-
const
|
|
6738
|
-
const
|
|
7013
|
+
const flags = `${s.scope === "system" ? " --system" : s.platform === "linux" ? " --user" : ""}${serviceName(s) ? ` --name ${serviceName(s)}` : ""}`;
|
|
7014
|
+
const manage = `Manage: ${cli} server status|restart|stop|uninstall${flags}`;
|
|
7015
|
+
const when = s.scope === "system" ? "at boot, as root (no linger)" : s.platform === "linux" ? "at boot or login" : "at login";
|
|
7016
|
+
const config = `Port and token: ${PORT_KEY} and ${TOKEN_KEY} in ${serviceConfigPath(s)} (edit them, then: ${cli} server restart${flags})`;
|
|
6739
7017
|
switch (result.status) {
|
|
6740
7018
|
case "started": {
|
|
6741
7019
|
const linger = lingerGuidance(result.linger);
|
|
@@ -6757,12 +7035,12 @@ ${manage}
|
|
|
6757
7035
|
${linger}`;
|
|
6758
7036
|
}
|
|
6759
7037
|
case "waiting-for-pi-login":
|
|
6760
|
-
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${
|
|
6761
|
-
Log in to Pi, then: ${cli} server restart
|
|
7038
|
+
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join12(agentDirectory(s), "auth.json")}.
|
|
7039
|
+
Log in to Pi, then: ${cli} server restart${flags}
|
|
6762
7040
|
${config}
|
|
6763
7041
|
${lingerGuidance(result.linger)}`;
|
|
6764
7042
|
case "stopped":
|
|
6765
|
-
return action === "stop" ? `Cadence stopped; it starts again ${when}, or now with: ${cli} server restart
|
|
7043
|
+
return action === "stop" ? `Cadence stopped; it starts again ${when}, or now with: ${cli} server restart${flags}
|
|
6766
7044
|
` : "";
|
|
6767
7045
|
case "absent":
|
|
6768
7046
|
return action === "restart" || action === "stop" ? `Cadence's background service is not installed: ${cli} server install
|
|
@@ -6771,7 +7049,22 @@ ${lingerGuidance(result.linger)}`;
|
|
|
6771
7049
|
return `Cadence's service definition was rewritten for ${result.workspace}; left disabled as configured.
|
|
6772
7050
|
`;
|
|
6773
7051
|
case "masked":
|
|
6774
|
-
return
|
|
7052
|
+
return `${s.unit} is masked; Cadence did not enable or start it.
|
|
7053
|
+
`;
|
|
7054
|
+
case "root-required":
|
|
7055
|
+
return `System service management requires root. Run this command explicitly as root (for example sudo -H); Cadence never elevates itself. Root uses its own Pi credentials and private state, not copied user profiles.
|
|
7056
|
+
`;
|
|
7057
|
+
case "scope-conflict":
|
|
7058
|
+
return `The user service ${result.unit} still exists. Stop/uninstall it explicitly with --user before installing --system; no service or state was migrated.
|
|
7059
|
+
`;
|
|
7060
|
+
case "project-running":
|
|
7061
|
+
return `This project already has a running owner. Stop it explicitly before registering a different supervisor: ${cli} stop --workspace ${workspace}
|
|
7062
|
+
`;
|
|
7063
|
+
case "environment-changed":
|
|
7064
|
+
return `HOME or XDG_STATE_HOME differs from the generated unit. Use its original environment for orderly stop/uninstall before explicitly installing with the new state selection; no migration or lock cleanup was attempted.
|
|
7065
|
+
`;
|
|
7066
|
+
case "workspace-changed":
|
|
7067
|
+
return `Workspace changed during orderly Stop; nothing was rewritten. Observe and retry explicitly.
|
|
6775
7068
|
`;
|
|
6776
7069
|
case "missing-pi-sdk":
|
|
6777
7070
|
return `Cadence did not register its service: the Pi SDK packages pi-coding-agent and pi-ai (scope @earendil-works) do not resolve from ${s.root}, for example after installing without optional dependencies; an owner without them would quarantine.
|
|
@@ -6781,7 +7074,7 @@ ${lingerGuidance(result.linger)}`;
|
|
|
6781
7074
|
`;
|
|
6782
7075
|
case "unavailable":
|
|
6783
7076
|
return result.workspace ? `Cadence changed nothing: its owner could not be observed. Inspect: ${cli} diagnose --workspace ${workspace}
|
|
6784
|
-
` : `No
|
|
7077
|
+
` : `No selected service manager; start Cadence explicitly: ${cli} start --workspace /absolute/project
|
|
6785
7078
|
`;
|
|
6786
7079
|
case "unsupported":
|
|
6787
7080
|
return `Cadence has no background service for ${s.platform}; start it explicitly: ${cli} start --workspace /absolute/project
|
|
@@ -6821,28 +7114,56 @@ async function serverMain(argv, hostUrl) {
|
|
|
6821
7114
|
argv.shift();
|
|
6822
7115
|
let requested;
|
|
6823
7116
|
let linger = process.env.CADENCE_LINGER !== "0";
|
|
6824
|
-
let
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
7117
|
+
let scope;
|
|
7118
|
+
let name, port;
|
|
7119
|
+
const seen = new Set;
|
|
7120
|
+
while (argv.length) {
|
|
7121
|
+
const flag = argv.shift();
|
|
7122
|
+
if (!flag || seen.has(flag))
|
|
7123
|
+
throw new ContractError("invalid");
|
|
7124
|
+
seen.add(flag);
|
|
7125
|
+
if (flag === "--system" || flag === "--user") {
|
|
7126
|
+
if (scope)
|
|
7127
|
+
throw new ContractError("invalid");
|
|
7128
|
+
scope = flag === "--system" ? "system" : "user";
|
|
7129
|
+
} else if (flag === "--no-linger" && action === "install")
|
|
6831
7130
|
linger = false;
|
|
7131
|
+
else if (flag === "--name" && argv[0])
|
|
7132
|
+
name = argv.shift();
|
|
7133
|
+
else if (flag === "--workspace" && action === "install" && argv[0])
|
|
7134
|
+
requested = resolve5(argv.shift());
|
|
7135
|
+
else if (flag === "--port" && action === "install" && argv[0]) {
|
|
7136
|
+
port = portValue(argv.shift());
|
|
7137
|
+
if (!port)
|
|
7138
|
+
throw new ContractError("invalid");
|
|
6832
7139
|
} else
|
|
6833
|
-
|
|
7140
|
+
throw new ContractError("invalid");
|
|
7141
|
+
}
|
|
7142
|
+
const s = hostSystem(dirname9(dirname9(fileURLToPath(hostUrl))), ownerControl, {
|
|
7143
|
+
scope,
|
|
7144
|
+
name
|
|
7145
|
+
});
|
|
7146
|
+
if ((scope || name !== undefined) && s.platform !== "linux")
|
|
7147
|
+
throw new ContractError("unsupported-context");
|
|
7148
|
+
if (s.scope === "system" && s.uid !== 0) {
|
|
7149
|
+
const result2 = { status: "root-required", scope: "system" };
|
|
7150
|
+
process.stderr.write(serverGuidance(s, action, result2, undefined));
|
|
7151
|
+
process.stdout.write(`${JSON.stringify(result2)}
|
|
7152
|
+
`);
|
|
7153
|
+
process.exitCode = 1;
|
|
7154
|
+
return;
|
|
6834
7155
|
}
|
|
6835
|
-
if (argv.length)
|
|
6836
|
-
throw new ContractError("invalid");
|
|
6837
|
-
const s = hostSystem(dirname9(dirname9(fileURLToPath(hostUrl))), ownerControl);
|
|
6838
7156
|
if (action === "install")
|
|
6839
7157
|
process.stderr.write(researchTemplate(s.home));
|
|
6840
7158
|
if (action === "install" || action === "restart") {
|
|
6841
7159
|
try {
|
|
6842
|
-
const { added, exposed } = ensureServerConfig(s.home
|
|
6843
|
-
|
|
7160
|
+
const { added, exposed } = ensureServerConfig(s.home, {
|
|
7161
|
+
name: serviceName(s),
|
|
7162
|
+
port
|
|
7163
|
+
});
|
|
7164
|
+
const path2 = serviceConfigPath(s);
|
|
6844
7165
|
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}
|
|
7166
|
+
process.stderr.write(`Cadence added ${added.map((key) => key === PORT_KEY ? `${key}=${port ?? DEFAULT_PORT}` : `a random private ${key}`).join(" and ")} to ${path2}
|
|
6846
7167
|
`);
|
|
6847
7168
|
if (exposed)
|
|
6848
7169
|
process.stderr.write(`Note: other users can read ${path2}; chmod 600 ${JSON.stringify(path2)} keeps ${TOKEN_KEY} private.
|
|
@@ -6878,10 +7199,72 @@ async function serverMain(argv, hostUrl) {
|
|
|
6878
7199
|
"unsupported",
|
|
6879
7200
|
"unconfirmed",
|
|
6880
7201
|
"quarantined",
|
|
6881
|
-
"missing-pi-sdk"
|
|
7202
|
+
"missing-pi-sdk",
|
|
7203
|
+
"root-required",
|
|
7204
|
+
"scope-conflict",
|
|
7205
|
+
"workspace-changed",
|
|
7206
|
+
"project-running",
|
|
7207
|
+
"environment-changed"
|
|
6882
7208
|
].includes(result.status) || result.status === "absent" && (action === "restart" || action === "stop"))
|
|
6883
7209
|
process.exitCode = 1;
|
|
6884
7210
|
}
|
|
7211
|
+
async function projectMain(argv, hostUrl) {
|
|
7212
|
+
if (argv.shift() !== "open")
|
|
7213
|
+
throw new ContractError("invalid");
|
|
7214
|
+
const options = { workspace: process.cwd() };
|
|
7215
|
+
let create = false, selected2 = false;
|
|
7216
|
+
const seen = new Set;
|
|
7217
|
+
while (argv.length) {
|
|
7218
|
+
const flag = argv.shift();
|
|
7219
|
+
if (!flag.startsWith("--") && !selected2) {
|
|
7220
|
+
options.workspace = resolve5(flag);
|
|
7221
|
+
selected2 = true;
|
|
7222
|
+
} else if (!seen.has(flag) && flag === "--create") {
|
|
7223
|
+
seen.add(flag);
|
|
7224
|
+
create = true;
|
|
7225
|
+
} else if (!seen.has(flag) && (flag === "--state-home" || flag === "--agent-dir") && argv[0]) {
|
|
7226
|
+
seen.add(flag);
|
|
7227
|
+
const value = resolve5(argv.shift());
|
|
7228
|
+
if (flag === "--state-home")
|
|
7229
|
+
options.stateHome = value;
|
|
7230
|
+
else
|
|
7231
|
+
options.agentDir = value;
|
|
7232
|
+
} else
|
|
7233
|
+
throw new ContractError("invalid");
|
|
7234
|
+
}
|
|
7235
|
+
if (privateWorkspace(options.workspace, options.stateHome))
|
|
7236
|
+
throw new ContractError("forbidden");
|
|
7237
|
+
if (create)
|
|
7238
|
+
mkdirSync7(options.workspace, { recursive: true });
|
|
7239
|
+
const landing = effectiveWorkspace(options.workspace, options.stateHome);
|
|
7240
|
+
if (landing.redirected) {
|
|
7241
|
+
process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
|
|
7242
|
+
`);
|
|
7243
|
+
mkdirSync7(landing.workspace, { recursive: true });
|
|
7244
|
+
}
|
|
7245
|
+
options.workspace = landing.workspace;
|
|
7246
|
+
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
7247
|
+
const kept = readHostConfig(c);
|
|
7248
|
+
if (!kept)
|
|
7249
|
+
options.port = 0;
|
|
7250
|
+
const result = await startService(options, false, hostUrl);
|
|
7251
|
+
const alive = result && typeof result === "object" && "status" in result && result.status === "alive";
|
|
7252
|
+
const config = alive ? readHostConfig(c) : undefined;
|
|
7253
|
+
process.stdout.write(`${JSON.stringify({ ...result, workspace: c.store.root, ...config ? { cwd: config.cwd, endpoint: c.localAccess().endpoint } : {} })}
|
|
7254
|
+
`);
|
|
7255
|
+
if (alive) {
|
|
7256
|
+
const link = await privateLink(c.store.root, options.stateHome);
|
|
7257
|
+
process.stderr.write(`Project owner ready: ${c.store.root} (task cwd: ${config?.cwd}). No task was created or started.
|
|
7258
|
+
`);
|
|
7259
|
+
if (link)
|
|
7260
|
+
process.stderr.write(`Private WebUI link: ${link}
|
|
7261
|
+
`);
|
|
7262
|
+
process.stderr.write(`Native entry: open Pi in this project and use /cadence. Stop only this project: cadence stop --workspace ${JSON.stringify(c.store.root)}${options.stateHome ? ` --state-home ${JSON.stringify(options.stateHome)}` : ""}
|
|
7263
|
+
`);
|
|
7264
|
+
}
|
|
7265
|
+
if (failedStatus(result))
|
|
7266
|
+
process.exitCode = 1;
|
|
7267
|
+
}
|
|
6885
7268
|
async function managementMain(argv, hostUrl) {
|
|
6886
7269
|
try {
|
|
6887
7270
|
if (!process.versions.node || process.versions.bun)
|
|
@@ -6890,12 +7273,19 @@ async function managementMain(argv, hostUrl) {
|
|
|
6890
7273
|
if (command === "--help" && !argv.length) {
|
|
6891
7274
|
process.stdout.write(`cadence start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
6892
7275
|
cadence server install [--workspace PATH] [--no-linger]|status|restart|stop|uninstall
|
|
6893
|
-
|
|
7276
|
+
Linux server options: --system (root, no linger) or --user; --name NAME selects an independent service; install also accepts --port PORT (required for a new named service). Root defaults to --system. No automatic sudo or profile migration.
|
|
7277
|
+
cadence project open [PATH] [--create] [--agent-dir PATH] [--state-home PATH]
|
|
7278
|
+
Project open explicitly starts/reuses that checkout owner, selecting a private free port for a new project. It never creates or starts a task.
|
|
7279
|
+
Start is explicit, or the background service that server install registers: a system/root unit on Linux without linger, or --user with optional linger (--no-linger or CADENCE_LINGER=0 keeps user scope 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).
|
|
6894
7280
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
6895
7281
|
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.
|
|
6896
7282
|
`);
|
|
6897
7283
|
return;
|
|
6898
7284
|
}
|
|
7285
|
+
if (command === "project") {
|
|
7286
|
+
await projectMain(argv, hostUrl);
|
|
7287
|
+
return;
|
|
7288
|
+
}
|
|
6899
7289
|
if (command === "server") {
|
|
6900
7290
|
await serverMain(argv, hostUrl);
|
|
6901
7291
|
return;
|
|
@@ -6966,12 +7356,12 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
6966
7356
|
`);
|
|
6967
7357
|
options.workspace = landing.workspace;
|
|
6968
7358
|
if (command === "start")
|
|
6969
|
-
|
|
7359
|
+
mkdirSync7(landing.workspace, { recursive: true });
|
|
6970
7360
|
}
|
|
6971
7361
|
let result;
|
|
6972
7362
|
if (command === "start")
|
|
6973
7363
|
result = await startService(options, foreground, hostUrl);
|
|
6974
|
-
else if (landing.redirected && !
|
|
7364
|
+
else if (landing.redirected && !existsSync3(landing.workspace)) {
|
|
6975
7365
|
if (command !== "status" && command !== "diagnose")
|
|
6976
7366
|
throw new ContractError("unavailable");
|
|
6977
7367
|
result = { status: "absent" };
|