@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 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
- export const SERVICE_LABEL = "io.miraland.conduit-runner";
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 join(homedir(), ".config", "conduit");
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", "conduit-runner.log");
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
- export function launchdPlist(programArguments, stdoutPath, pathEnv = runnerServicePath()) {
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", `${SERVICE_LABEL}.plist`);
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", "conduit-runner.service");
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", "conduit-runner.service.d");
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", "conduit-runner.service", "--property=DropInPaths", "--value"], { encoding: "utf8" });
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", "conduit-runner.service"], { encoding: "utf8" });
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
- "Run ops install to realign the service, or remove stale systemd drop-ins under conduit-runner.service.d/.");
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", "conduit-runner.service"],
293
- ...(!insideRunner ? [["--user", "restart", "conduit-runner.service"]] : []),
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
- run("schtasks", ["/Create", "/TN", WINDOWS_TASK_NAME, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"]);
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", WINDOWS_TASK_NAME]);
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: WINDOWS_TASK_NAME, platform: "win32" };
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(logPath()), { recursive: true });
344
- await writeFile(plistPath, launchdPlist(programArguments, logPath()), { mode: 0o644 });
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, logPath())], { detached: true, stdio: "ignore" }).unref();
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 ${SERVICE_LABEL}; see ${logPath()}`);
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 conduit-runner.service drop-in(s): ${dropIns.join(", ")}. ` +
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
- spawnSync("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], { encoding: "utf8" });
467
+ const taskName = windowsTaskNameFor(serviceName);
468
+ spawnSync("schtasks", ["/Delete", "/TN", taskName, "/F"], { encoding: "utf8" });
400
469
  await unlink(serviceStatePath()).catch(() => undefined);
401
- return { path: WINDOWS_TASK_NAME, platform: "win32" };
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}/${SERVICE_LABEL}`], { encoding: "utf8" });
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", "conduit-runner.service"], { encoding: "utf8" });
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.119",
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": {