@miraland-labs/conduit-bridge 0.16.119 → 0.16.121
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/dist/cli.js +2 -0
- package/dist/config.js +5 -0
- package/dist/ops.js +9 -3
- package/dist/service.js +115 -46
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -339,6 +339,7 @@ async function installService() {
|
|
|
339
339
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
340
340
|
workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
|
|
341
341
|
"ensure-checkout": { type: "string" }, "bootstrap-result": { type: "string" },
|
|
342
|
+
service: { type: "string" },
|
|
342
343
|
} });
|
|
343
344
|
if (!values.workspace) {
|
|
344
345
|
throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
|
|
@@ -363,6 +364,7 @@ async function installService() {
|
|
|
363
364
|
agentTimeoutMinutes: values["agent-timeout-minutes"],
|
|
364
365
|
ensureCheckout: values["ensure-checkout"],
|
|
365
366
|
bootstrapResult,
|
|
367
|
+
...(values.service ? { serviceName: values.service } : {}),
|
|
366
368
|
});
|
|
367
369
|
console.log(`Installed Conduit Bridge v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION}) runner service (${result.platform}): ${result.path}`);
|
|
368
370
|
console.log(`Online lanes: ${onlineDriverIds(config).join(", ")}. Toggle with \`${bridgeUsage("drivers", "online|offline", "…")}\`.`);
|
package/dist/config.js
CHANGED
|
@@ -18,6 +18,11 @@ export function clampLeaseCapacity(value) {
|
|
|
18
18
|
const directory = process.env.CONDUIT_BRIDGE_CONFIG_DIR?.trim()
|
|
19
19
|
? resolve(process.env.CONDUIT_BRIDGE_CONFIG_DIR)
|
|
20
20
|
: join(homedir(), ".config", "conduit");
|
|
21
|
+
/** The installation this process belongs to. Service state follows it, or a second installation on
|
|
22
|
+
* one computer writes its service record over the first one's. */
|
|
23
|
+
export function bridgeConfigDirectory() {
|
|
24
|
+
return directory;
|
|
25
|
+
}
|
|
21
26
|
const path = join(directory, "config.json");
|
|
22
27
|
/** Runtime attempt/session state — separate so drivers CLI cannot clobber live claims. */
|
|
23
28
|
const runtimePath = join(directory, "runtime.json");
|
package/dist/ops.js
CHANGED
|
@@ -15,7 +15,7 @@ import { ensureCheckout } from "./checkout.js";
|
|
|
15
15
|
import { detectInstalledClients, probeAgentHealth } from "./detect.js";
|
|
16
16
|
import { driverIdsFromDetectedLabels, formatLaneTiers, laneDispatchBlock, laneStatuses } from "./drivers.js";
|
|
17
17
|
import { BRIDGE_PROTOCOL_VERSION, describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
18
|
-
import { applyRunnerToolPath, readActiveRunnerServiceWorkspace, readStoredRunnerServiceOptions, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
18
|
+
import { applyRunnerToolPath, readActiveRunnerServiceWorkspace, readStoredRunnerServiceOptions, readStoredServiceName, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
19
19
|
import { bridgeVersion } from "./version.js";
|
|
20
20
|
import { bootstrapManagedWorkspace } from "./workspace-bootstrap.js";
|
|
21
21
|
import { managedWorkspacePath } from "./managed-workspace-path.js";
|
|
@@ -351,7 +351,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
351
351
|
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
352
352
|
const workspace = resolve(expandOpsValue(workspaceRaw));
|
|
353
353
|
if (!values["skip-service-alignment"]) {
|
|
354
|
-
for (const warning of runnerServiceWorkspaceWarnings(workspace)) {
|
|
354
|
+
for (const warning of runnerServiceWorkspaceWarnings(workspace, homedir(), await readStoredServiceName())) {
|
|
355
355
|
console.warn(`WARNING: ${warning}`);
|
|
356
356
|
}
|
|
357
357
|
}
|
|
@@ -379,7 +379,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
379
379
|
console.log("");
|
|
380
380
|
}
|
|
381
381
|
if (report.ready) {
|
|
382
|
-
const serviceWarnings = values["skip-service-alignment"] ? [] : runnerServiceWorkspaceWarnings(workspace);
|
|
382
|
+
const serviceWarnings = values["skip-service-alignment"] ? [] : runnerServiceWorkspaceWarnings(workspace, homedir(), await readStoredServiceName());
|
|
383
383
|
if (serviceWarnings.length > 0) {
|
|
384
384
|
for (const warning of serviceWarnings)
|
|
385
385
|
console.warn(`WARNING: ${warning}`);
|
|
@@ -408,16 +408,20 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
408
408
|
// seed ops.env once; later project changes must not mutate it.
|
|
409
409
|
let installEnv = env;
|
|
410
410
|
let installArgv = argv;
|
|
411
|
+
let installServiceName = null;
|
|
411
412
|
if (verb === "install") {
|
|
412
413
|
const { values, positionals } = parseArgs({
|
|
413
414
|
args: argv,
|
|
414
415
|
options: {
|
|
415
416
|
workspace: { type: "string" },
|
|
416
417
|
repo: { type: "string" },
|
|
418
|
+
// The supervisor name this installation's service carries; one computer may hold two.
|
|
419
|
+
service: { type: "string" },
|
|
417
420
|
},
|
|
418
421
|
allowPositionals: true,
|
|
419
422
|
});
|
|
420
423
|
installArgv = positionals;
|
|
424
|
+
installServiceName = values.service?.trim() || null;
|
|
421
425
|
const declaredWorkspace = values.workspace?.trim();
|
|
422
426
|
const declaredRepo = values.repo?.trim();
|
|
423
427
|
if (declaredWorkspace || declaredRepo) {
|
|
@@ -670,6 +674,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
670
674
|
]);
|
|
671
675
|
}
|
|
672
676
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
677
|
+
if (installServiceName)
|
|
678
|
+
installArgs.push("--service", installServiceName);
|
|
673
679
|
if (installEnv.CONDUIT_REPO)
|
|
674
680
|
installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
675
681
|
if (bootstrapResult)
|
package/dist/service.js
CHANGED
|
@@ -3,15 +3,38 @@ import { homedir, platform } from "node:os";
|
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
-
|
|
6
|
+
import { bridgeConfigDirectory } from "./config.js";
|
|
7
|
+
/** The name an installation's service carries when its operator named none. */
|
|
8
|
+
export const DEFAULT_SERVICE_NAME = "conduit-runner";
|
|
9
|
+
/** Backwards-compatible label for the default installation (macOS). */
|
|
10
|
+
export const SERVICE_LABEL = `io.miraland.${DEFAULT_SERVICE_NAME}`;
|
|
11
|
+
/**
|
|
12
|
+
* One computer may hold more than one installation — this host runs the Conduit lane and the forge
|
|
13
|
+
* lane — and a service named after the product cannot say which work it serves. The name is a
|
|
14
|
+
* property of the installation: given once at install time, stored beside its program arguments, and
|
|
15
|
+
* read back by every path that looks the service up.
|
|
16
|
+
*/
|
|
17
|
+
const SERVICE_NAME_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
18
|
+
export function assertServiceName(name) {
|
|
19
|
+
if (!SERVICE_NAME_PATTERN.test(name)) {
|
|
20
|
+
throw new Error(`Service name must match ${SERVICE_NAME_PATTERN} (got "${name}") — no .service suffix, no spaces.`);
|
|
21
|
+
}
|
|
22
|
+
return name;
|
|
23
|
+
}
|
|
24
|
+
export function serviceLabelFor(name) {
|
|
25
|
+
return `io.miraland.${name}`;
|
|
26
|
+
}
|
|
27
|
+
export function windowsTaskNameFor(name) {
|
|
28
|
+
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
29
|
+
}
|
|
7
30
|
function configDir() {
|
|
8
|
-
return
|
|
31
|
+
return bridgeConfigDirectory();
|
|
9
32
|
}
|
|
10
33
|
function serviceStatePath() {
|
|
11
34
|
return join(configDir(), "service.json");
|
|
12
35
|
}
|
|
13
|
-
function logPath() {
|
|
14
|
-
return join(homedir(), "Library", "Logs",
|
|
36
|
+
function logPath(name = DEFAULT_SERVICE_NAME) {
|
|
37
|
+
return join(homedir(), "Library", "Logs", `${name}.log`);
|
|
15
38
|
}
|
|
16
39
|
function linuxLogDir() {
|
|
17
40
|
return join(homedir(), ".local", "state", "conduit");
|
|
@@ -67,7 +90,17 @@ export function applyRunnerToolPath(env = process.env, home = homedir(), nodeBin
|
|
|
67
90
|
env.PATH = next;
|
|
68
91
|
return next;
|
|
69
92
|
}
|
|
70
|
-
|
|
93
|
+
/**
|
|
94
|
+
* A service must run the installation that installed it. Without this the unit started with the
|
|
95
|
+
* default configuration directory, so the second lane's service ran the first lane's credentials —
|
|
96
|
+
* seen on the trial host the moment two lanes existed.
|
|
97
|
+
*/
|
|
98
|
+
export function installationEnvironment(configDirectory = configDir()) {
|
|
99
|
+
return resolve(configDirectory) === resolve(join(homedir(), ".config", "conduit"))
|
|
100
|
+
? {}
|
|
101
|
+
: { CONDUIT_BRIDGE_CONFIG_DIR: configDirectory };
|
|
102
|
+
}
|
|
103
|
+
export function launchdPlist(programArguments, stdoutPath, pathEnv = runnerServicePath(), environment = {}) {
|
|
71
104
|
const argsXml = programArguments.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
|
|
72
105
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
73
106
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -83,6 +116,7 @@ ${argsXml}
|
|
|
83
116
|
<dict>
|
|
84
117
|
<key>PATH</key>
|
|
85
118
|
<string>${escapeXml(pathEnv)}</string>
|
|
119
|
+
${Object.entries(environment).map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`).join("\n")}
|
|
86
120
|
</dict>
|
|
87
121
|
<key>RunAtLoad</key>
|
|
88
122
|
<true/>
|
|
@@ -96,7 +130,7 @@ ${argsXml}
|
|
|
96
130
|
</plist>
|
|
97
131
|
`;
|
|
98
132
|
}
|
|
99
|
-
export function systemdUserUnit(programArguments, workingDirectory, pathEnv = runnerServicePath()) {
|
|
133
|
+
export function systemdUserUnit(programArguments, workingDirectory, pathEnv = runnerServicePath(), environment = {}) {
|
|
100
134
|
const execStart = programArguments.map(escapeSystemdExecArgument).join(" ");
|
|
101
135
|
return `[Unit]
|
|
102
136
|
Description=Conduit Bridge runner
|
|
@@ -108,6 +142,7 @@ Type=simple
|
|
|
108
142
|
WorkingDirectory=${escapeSystemdUnitValue(workingDirectory)}
|
|
109
143
|
Environment=PATH=${escapeSystemdUnitValue(pathEnv)}
|
|
110
144
|
Environment=CONDUIT_RUNNER_SERVICE=systemd
|
|
145
|
+
${Object.entries(environment).map(([key, value]) => `Environment=${key}=${escapeSystemdUnitValue(value)}`).join("\n")}
|
|
111
146
|
ExecStart=${execStart}
|
|
112
147
|
Restart=always
|
|
113
148
|
RestartSec=5
|
|
@@ -130,14 +165,14 @@ function escapeSystemdExecArgument(value) {
|
|
|
130
165
|
// Exec*= additionally expands $ environment references; $$ passes one literal dollar.
|
|
131
166
|
return escapeSystemdUnitValue(value.replaceAll("$", () => "$$"));
|
|
132
167
|
}
|
|
133
|
-
function launchAgentsPath() {
|
|
134
|
-
return join(homedir(), "Library", "LaunchAgents", `${
|
|
168
|
+
function launchAgentsPath(name = DEFAULT_SERVICE_NAME) {
|
|
169
|
+
return join(homedir(), "Library", "LaunchAgents", `${serviceLabelFor(name)}.plist`);
|
|
135
170
|
}
|
|
136
|
-
export function systemdUnitPath(home = homedir()) {
|
|
137
|
-
return join(home, ".config", "systemd", "user",
|
|
171
|
+
export function systemdUnitPath(home = homedir(), name = DEFAULT_SERVICE_NAME) {
|
|
172
|
+
return join(home, ".config", "systemd", "user", `${name}.service`);
|
|
138
173
|
}
|
|
139
|
-
export function systemdDropInDir(home = homedir()) {
|
|
140
|
-
return join(home, ".config", "systemd", "user",
|
|
174
|
+
export function systemdDropInDir(home = homedir(), name = DEFAULT_SERVICE_NAME) {
|
|
175
|
+
return join(home, ".config", "systemd", "user", `${name}.service.d`);
|
|
141
176
|
}
|
|
142
177
|
/** Parse `--workspace` from a runner argv list or systemd ExecStart line. */
|
|
143
178
|
export function parseRunnerWorkspaceFromProgramArguments(args) {
|
|
@@ -154,9 +189,9 @@ export function parseSystemdDropInPaths(value) {
|
|
|
154
189
|
return value.trim().split(/\s+/).filter((path) => path.endsWith(".conf"));
|
|
155
190
|
}
|
|
156
191
|
/** Any effective drop-in can change workspace, identity, fuel, PATH, or restart behaviour. */
|
|
157
|
-
export async function systemdDropInConflicts(home = homedir()) {
|
|
192
|
+
export async function systemdDropInConflicts(home = homedir(), name = DEFAULT_SERVICE_NAME) {
|
|
158
193
|
const conflicts = new Set();
|
|
159
|
-
const dir = systemdDropInDir(home);
|
|
194
|
+
const dir = systemdDropInDir(home, name);
|
|
160
195
|
if (existsSync(dir)) {
|
|
161
196
|
for (const name of await readdir(dir)) {
|
|
162
197
|
if (name.endsWith(".conf"))
|
|
@@ -164,7 +199,7 @@ export async function systemdDropInConflicts(home = homedir()) {
|
|
|
164
199
|
}
|
|
165
200
|
}
|
|
166
201
|
if (platform() === "linux") {
|
|
167
|
-
const effective = spawnSync("systemctl", ["--user", "show",
|
|
202
|
+
const effective = spawnSync("systemctl", ["--user", "show", `${name}.service`, "--property=DropInPaths", "--value"], { encoding: "utf8" });
|
|
168
203
|
if (effective.status === 0) {
|
|
169
204
|
for (const path of parseSystemdDropInPaths(effective.stdout))
|
|
170
205
|
conflicts.add(basename(path));
|
|
@@ -172,6 +207,16 @@ export async function systemdDropInConflicts(home = homedir()) {
|
|
|
172
207
|
}
|
|
173
208
|
return [...conflicts].sort();
|
|
174
209
|
}
|
|
210
|
+
/** The name this installation's service carries, from its own record. */
|
|
211
|
+
export async function readStoredServiceName() {
|
|
212
|
+
try {
|
|
213
|
+
const stored = JSON.parse(await readFile(serviceStatePath(), "utf8"));
|
|
214
|
+
return stored.serviceName && SERVICE_NAME_PATTERN.test(stored.serviceName) ? stored.serviceName : DEFAULT_SERVICE_NAME;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return DEFAULT_SERVICE_NAME;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
175
220
|
export async function readStoredRunnerServiceOptions() {
|
|
176
221
|
try {
|
|
177
222
|
const stored = JSON.parse(await readFile(serviceStatePath(), "utf8"));
|
|
@@ -191,13 +236,13 @@ export async function readStoredRunnerServiceWorkspace() {
|
|
|
191
236
|
}
|
|
192
237
|
/** Installed supervisor command is the active workspace authority; service.json is fallback. */
|
|
193
238
|
export async function readActiveRunnerServiceWorkspace(home = homedir()) {
|
|
194
|
-
return readEffectiveLinuxRunnerWorkspace(home) ?? await readStoredRunnerServiceWorkspace();
|
|
239
|
+
return readEffectiveLinuxRunnerWorkspace(home, await readStoredServiceName()) ?? await readStoredRunnerServiceWorkspace();
|
|
195
240
|
}
|
|
196
241
|
/** Effective workspace from the installed supervisor (systemd user unit on Linux). */
|
|
197
|
-
export function readEffectiveLinuxRunnerWorkspace(home = homedir()) {
|
|
242
|
+
export function readEffectiveLinuxRunnerWorkspace(home = homedir(), name = DEFAULT_SERVICE_NAME) {
|
|
198
243
|
if (platform() !== "linux")
|
|
199
244
|
return null;
|
|
200
|
-
const result = spawnSync("systemctl", ["--user", "cat",
|
|
245
|
+
const result = spawnSync("systemctl", ["--user", "cat", `${name}.service`], { encoding: "utf8" });
|
|
201
246
|
if (result.status !== 0)
|
|
202
247
|
return null;
|
|
203
248
|
let execStart = null;
|
|
@@ -207,13 +252,13 @@ export function readEffectiveLinuxRunnerWorkspace(home = homedir()) {
|
|
|
207
252
|
}
|
|
208
253
|
return execStart ? parseRunnerWorkspaceFromExecStart(execStart) : null;
|
|
209
254
|
}
|
|
210
|
-
export function runnerServiceWorkspaceWarnings(declaredWorkspace, home = homedir()) {
|
|
255
|
+
export function runnerServiceWorkspaceWarnings(declaredWorkspace, home = homedir(), name = DEFAULT_SERVICE_NAME) {
|
|
211
256
|
const declared = resolve(declaredWorkspace);
|
|
212
257
|
const warnings = [];
|
|
213
|
-
const stored = platform() === "linux" ? readEffectiveLinuxRunnerWorkspace(home) : null;
|
|
258
|
+
const stored = platform() === "linux" ? readEffectiveLinuxRunnerWorkspace(home, name) : null;
|
|
214
259
|
if (stored && resolve(stored) !== declared) {
|
|
215
260
|
warnings.push(`Background runner service uses workspace ${stored}, but ops.env declares ${declaredWorkspace}. ` +
|
|
216
|
-
|
|
261
|
+
`Run ops install to realign the service, or remove stale systemd drop-ins under ${name}.service.d/.`);
|
|
217
262
|
}
|
|
218
263
|
return warnings;
|
|
219
264
|
}
|
|
@@ -287,10 +332,10 @@ export function runningInsideRunnerContainer(env = process.env) {
|
|
|
287
332
|
* explicitly; the resident runner must return to its caller and exit normally, after which
|
|
288
333
|
* Restart=always starts the rewritten unit.
|
|
289
334
|
*/
|
|
290
|
-
export function systemdActivationCommands(insideRunner) {
|
|
335
|
+
export function systemdActivationCommands(insideRunner, name = DEFAULT_SERVICE_NAME) {
|
|
291
336
|
return [
|
|
292
|
-
["--user", "enable",
|
|
293
|
-
...(!insideRunner ? [["--user", "restart",
|
|
337
|
+
["--user", "enable", `${name}.service`],
|
|
338
|
+
...(!insideRunner ? [["--user", "restart", `${name}.service`]] : []),
|
|
294
339
|
];
|
|
295
340
|
}
|
|
296
341
|
/**
|
|
@@ -315,6 +360,18 @@ export function reloadLaunchdRunnerSync(domain, plistPath) {
|
|
|
315
360
|
spawnSync("launchctl", ["kickstart", "-k", label], { encoding: "utf8" });
|
|
316
361
|
return launchdServiceLoaded(domain);
|
|
317
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* A unit already written by another installation must not be taken over: that is how one lane's
|
|
365
|
+
* service silently started serving another lane's workspace. The unit records the installation it
|
|
366
|
+
* belongs to in its WorkingDirectory.
|
|
367
|
+
*/
|
|
368
|
+
export function unitBelongsToAnotherInstallation(unitText, installation) {
|
|
369
|
+
const line = unitText.split(/\r?\n/).find((entry) => entry.startsWith("WorkingDirectory="));
|
|
370
|
+
if (!line)
|
|
371
|
+
return null;
|
|
372
|
+
const owner = line.slice("WorkingDirectory=".length).trim().replace(/^"|"$/g, "");
|
|
373
|
+
return owner && resolve(owner) !== resolve(installation) ? owner : null;
|
|
374
|
+
}
|
|
318
375
|
export async function installRunnerService(options = {}) {
|
|
319
376
|
if (runningInsideRunnerContainer()) {
|
|
320
377
|
return { path: "container entrypoint", platform: "container" };
|
|
@@ -323,30 +380,34 @@ export async function installRunnerService(options = {}) {
|
|
|
323
380
|
if (host !== "darwin" && host !== "linux" && host !== "win32") {
|
|
324
381
|
throw new Error(`install-service is supported on macOS, Linux, and Windows only (got ${host})`);
|
|
325
382
|
}
|
|
383
|
+
const serviceName = assertServiceName(options.serviceName ?? await readStoredServiceName());
|
|
384
|
+
const stored = { ...options, serviceName };
|
|
326
385
|
const programArguments = runnerProgramArguments(options);
|
|
327
386
|
if (host === "win32") {
|
|
328
387
|
const tr = windowsTaskCommandLine(programArguments);
|
|
329
|
-
|
|
388
|
+
const taskName = windowsTaskNameFor(serviceName);
|
|
389
|
+
run("schtasks", ["/Create", "/TN", taskName, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"]);
|
|
330
390
|
try {
|
|
331
|
-
run("schtasks", ["/Run", "/TN",
|
|
391
|
+
run("schtasks", ["/Run", "/TN", taskName]);
|
|
332
392
|
}
|
|
333
393
|
catch {
|
|
334
394
|
// Task is registered for next logon. /Run needs an interactive session; enroll already succeeded.
|
|
335
395
|
}
|
|
336
396
|
await mkdir(configDir(), { recursive: true });
|
|
337
|
-
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "win32", programArguments, options, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
338
|
-
return { path:
|
|
397
|
+
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "win32", serviceName, programArguments, options: stored, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
398
|
+
return { path: taskName, platform: "win32" };
|
|
339
399
|
}
|
|
340
400
|
if (host === "darwin") {
|
|
341
|
-
const plistPath = launchAgentsPath();
|
|
401
|
+
const plistPath = launchAgentsPath(serviceName);
|
|
402
|
+
const log = logPath(serviceName);
|
|
342
403
|
await mkdir(dirname(plistPath), { recursive: true });
|
|
343
|
-
await mkdir(dirname(
|
|
344
|
-
await writeFile(plistPath, launchdPlist(programArguments,
|
|
404
|
+
await mkdir(dirname(log), { recursive: true });
|
|
405
|
+
await writeFile(plistPath, launchdPlist(programArguments, log, runnerServicePath(), installationEnvironment()), { mode: 0o644 });
|
|
345
406
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
346
407
|
let loaded = false;
|
|
347
408
|
if (runningInsideRunnerLaunchAgent()) {
|
|
348
409
|
// Detached helper must survive bootout of this process.
|
|
349
|
-
spawn("/bin/sh", ["-c", launchdSwapCommand(domain, plistPath,
|
|
410
|
+
spawn("/bin/sh", ["-c", launchdSwapCommand(domain, plistPath, log)], { detached: true, stdio: "ignore" }).unref();
|
|
350
411
|
for (let attempt = 0; attempt < 20 && !loaded; attempt++) {
|
|
351
412
|
await sleepMs(attempt === 0 ? 2000 : 500);
|
|
352
413
|
loaded = launchdServiceLoaded(domain);
|
|
@@ -363,24 +424,30 @@ export async function installRunnerService(options = {}) {
|
|
|
363
424
|
}
|
|
364
425
|
}
|
|
365
426
|
if (!loaded)
|
|
366
|
-
throw new Error(`launchd did not load ${
|
|
367
|
-
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "darwin", programArguments, options, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
427
|
+
throw new Error(`launchd did not load ${serviceLabelFor(serviceName)}; see ${log}`);
|
|
428
|
+
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "darwin", serviceName, programArguments, options: stored, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
368
429
|
return { path: plistPath, platform: "darwin" };
|
|
369
430
|
}
|
|
370
|
-
const unitPath = systemdUnitPath();
|
|
371
|
-
const dropIns = await systemdDropInConflicts();
|
|
431
|
+
const unitPath = systemdUnitPath(homedir(), serviceName);
|
|
432
|
+
const dropIns = await systemdDropInConflicts(homedir(), serviceName);
|
|
372
433
|
if (dropIns.length) {
|
|
373
|
-
throw new Error(`Refusing to install with
|
|
434
|
+
throw new Error(`Refusing to install with ${serviceName}.service drop-in(s): ${dropIns.join(", ")}. ` +
|
|
374
435
|
"Review and migrate or remove them, then run ops install again.");
|
|
375
436
|
}
|
|
437
|
+
const existing = existsSync(unitPath) ? await readFile(unitPath, "utf8") : "";
|
|
438
|
+
const otherOwner = existing ? unitBelongsToAnotherInstallation(existing, configDir()) : null;
|
|
439
|
+
if (otherOwner) {
|
|
440
|
+
throw new Error(`${serviceName}.service already belongs to the installation at ${otherOwner}. ` +
|
|
441
|
+
`Install this one under another name: ops install --service <name>.`);
|
|
442
|
+
}
|
|
376
443
|
await mkdir(dirname(unitPath), { recursive: true });
|
|
377
444
|
await mkdir(linuxLogDir(), { recursive: true });
|
|
378
|
-
await writeFile(unitPath, systemdUserUnit(programArguments, configDir()), { mode: 0o644 });
|
|
445
|
+
await writeFile(unitPath, systemdUserUnit(programArguments, configDir(), runnerServicePath(), installationEnvironment()), { mode: 0o644 });
|
|
379
446
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
380
|
-
for (const args of systemdActivationCommands(runningInsideRunnerSystemd())) {
|
|
447
|
+
for (const args of systemdActivationCommands(runningInsideRunnerSystemd(), serviceName)) {
|
|
381
448
|
run("systemctl", args);
|
|
382
449
|
}
|
|
383
|
-
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "linux", programArguments, options, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
450
|
+
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "linux", serviceName, programArguments, options: stored, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
384
451
|
return { path: unitPath, platform: "linux" };
|
|
385
452
|
}
|
|
386
453
|
export async function uninstallRunnerService() {
|
|
@@ -395,21 +462,23 @@ export async function uninstallRunnerService() {
|
|
|
395
462
|
catch {
|
|
396
463
|
stored = null;
|
|
397
464
|
}
|
|
465
|
+
const serviceName = await readStoredServiceName();
|
|
398
466
|
if (host === "win32") {
|
|
399
|
-
|
|
467
|
+
const taskName = windowsTaskNameFor(serviceName);
|
|
468
|
+
spawnSync("schtasks", ["/Delete", "/TN", taskName, "/F"], { encoding: "utf8" });
|
|
400
469
|
await unlink(serviceStatePath()).catch(() => undefined);
|
|
401
|
-
return { path:
|
|
470
|
+
return { path: taskName, platform: "win32" };
|
|
402
471
|
}
|
|
403
472
|
if (host === "darwin") {
|
|
404
|
-
const plistPath = launchAgentsPath();
|
|
405
|
-
spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${
|
|
473
|
+
const plistPath = launchAgentsPath(serviceName);
|
|
474
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${serviceLabelFor(serviceName)}`], { encoding: "utf8" });
|
|
406
475
|
spawnSync("launchctl", ["unload", "-w", plistPath], { encoding: "utf8" });
|
|
407
476
|
await unlink(plistPath).catch(() => undefined);
|
|
408
477
|
await unlink(serviceStatePath()).catch(() => undefined);
|
|
409
478
|
return { path: plistPath, platform: stored?.platform ?? "darwin" };
|
|
410
479
|
}
|
|
411
|
-
const unitPath = systemdUnitPath();
|
|
412
|
-
spawnSync("systemctl", ["--user", "disable", "--now",
|
|
480
|
+
const unitPath = systemdUnitPath(homedir(), serviceName);
|
|
481
|
+
spawnSync("systemctl", ["--user", "disable", "--now", `${serviceName}.service`], { encoding: "utf8" });
|
|
413
482
|
await unlink(unitPath).catch(() => undefined);
|
|
414
483
|
spawnSync("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
|
|
415
484
|
await unlink(serviceStatePath()).catch(() => undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.121",
|
|
4
4
|
"description": "Conduit Bridge CLI \u2014 join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|