@miraland-labs/conduit-bridge 0.11.11 → 0.12.0
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 +1 -1
- package/dist/cli.js +1 -1
- package/dist/ops.js +63 -17
- package/dist/service.js +29 -6
- package/ops/env.example +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Optional local shortcuts after `init-ops` (same verbs):
|
|
|
51
51
|
|
|
52
52
|
**Windows note:** `ops install` brings lanes online and prints a `runner --workspace …` command to keep open (no LaunchAgent). macOS/Linux install a background service.
|
|
53
53
|
|
|
54
|
-
`ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default
|
|
54
|
+
`ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default: auto-detect installed agents), `CONDUIT_ROLES` (default `implement research review`). Values expand `$HOME`, `%USERPROFILE%`, and `~`.
|
|
55
55
|
|
|
56
56
|
## Advanced CLI
|
|
57
57
|
|
package/dist/cli.js
CHANGED
|
@@ -359,7 +359,7 @@ async function initOps() {
|
|
|
359
359
|
async function opsCommand() {
|
|
360
360
|
const verb = process.argv[3];
|
|
361
361
|
if (!verb || !OPS_VERBS.includes(verb)) {
|
|
362
|
-
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")}`);
|
|
362
|
+
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")} (install also takes --workspace <path> [--repo <url>])`);
|
|
363
363
|
}
|
|
364
364
|
await runOps(verb, process.argv.slice(4));
|
|
365
365
|
}
|
package/dist/ops.js
CHANGED
|
@@ -9,6 +9,8 @@ import { dirname, join, resolve } from "node:path";
|
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import { ConduitClient } from "./client.js";
|
|
11
11
|
import { loadConfig } from "./config.js";
|
|
12
|
+
import { detectInstalledClients } from "./detect.js";
|
|
13
|
+
import { driverIdsFromDetectedLabels } from "./drivers.js";
|
|
12
14
|
import { describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
13
15
|
export const OPS_VERBS = [
|
|
14
16
|
"connect", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
@@ -73,7 +75,7 @@ export function loadOpsEnv(home = homedir(), cwd = process.cwd()) {
|
|
|
73
75
|
CONDUIT_ORG: pick("CONDUIT_ORG", ""),
|
|
74
76
|
CONDUIT_WORKSPACE: pick("CONDUIT_WORKSPACE", ""),
|
|
75
77
|
CONDUIT_REPO: pick("CONDUIT_REPO", ""),
|
|
76
|
-
CONDUIT_DRIVERS: pick("CONDUIT_DRIVERS", "
|
|
78
|
+
CONDUIT_DRIVERS: pick("CONDUIT_DRIVERS", ""),
|
|
77
79
|
CONDUIT_ROLES: pick("CONDUIT_ROLES", "implement research review"),
|
|
78
80
|
loadedFrom,
|
|
79
81
|
};
|
|
@@ -120,12 +122,21 @@ export function writeOpsEnvFile(path, patch, existingText) {
|
|
|
120
122
|
}
|
|
121
123
|
writeFileSync(path, `${next.filter((line, index) => !(index === next.length - 1 && line === "")).join("\n").replace(/\n*$/, "\n")}`, { mode: 0o600 });
|
|
122
124
|
}
|
|
123
|
-
|
|
125
|
+
/** Explicit ids win; otherwise CONDUIT_DRIVERS; otherwise agents detected on PATH (fail when none). */
|
|
126
|
+
export async function resolveDrivers(env, argv, detect = detectInstalledClients) {
|
|
124
127
|
const fromArgs = argv.map((item) => item.trim()).filter(Boolean);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
if (fromArgs.length)
|
|
129
|
+
return fromArgs;
|
|
130
|
+
const fromEnv = splitOpsList(env.CONDUIT_DRIVERS);
|
|
131
|
+
if (fromEnv.length)
|
|
132
|
+
return fromEnv;
|
|
133
|
+
const detected = driverIdsFromDetectedLabels(await detect());
|
|
134
|
+
if (detected.length) {
|
|
135
|
+
console.log(`CONDUIT_DRIVERS not set — using detected agents: ${detected.join(", ")}`);
|
|
136
|
+
return detected;
|
|
137
|
+
}
|
|
138
|
+
throw new Error("No coding agent found on this computer. Install one (Claude Code, Codex, Cursor, OpenCode, Pi, Kiro, Antigravity), " +
|
|
139
|
+
"then retry — or set CONDUIT_DRIVERS / pass driver ids explicitly.");
|
|
129
140
|
}
|
|
130
141
|
/** Quote args for a copy-pasteable shell/cmd line (paths with spaces). */
|
|
131
142
|
export function shellQuoteArgs(args) {
|
|
@@ -171,7 +182,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
171
182
|
console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
|
|
172
183
|
console.log(`Workspace: ${env.CONDUIT_WORKSPACE || "(unset)"}`);
|
|
173
184
|
console.log(`Repo: ${env.CONDUIT_REPO || "(none)"}`);
|
|
174
|
-
console.log(`Drivers: ${env.CONDUIT_DRIVERS}`);
|
|
185
|
+
console.log(`Drivers: ${env.CONDUIT_DRIVERS || "(auto-detect installed agents)"}`);
|
|
175
186
|
console.log(`Roles: ${env.CONDUIT_ROLES}`);
|
|
176
187
|
console.log("");
|
|
177
188
|
}
|
|
@@ -202,11 +213,45 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
202
213
|
}
|
|
203
214
|
// Lane toggles only need Bridge config + optional driver ids — not a full ops.env.
|
|
204
215
|
if (verb === "online" || verb === "offline") {
|
|
205
|
-
const drivers = resolveDrivers(env, argv);
|
|
216
|
+
const drivers = await resolveDrivers(env, argv);
|
|
206
217
|
runBridge(["drivers", verb, ...drivers]);
|
|
207
218
|
return;
|
|
208
219
|
}
|
|
209
|
-
|
|
220
|
+
// `ops install --workspace <path> [--repo <url>]` persists declared intent into ops.env so the
|
|
221
|
+
// Connect UI path works without hand-editing a config file first.
|
|
222
|
+
let installEnv = env;
|
|
223
|
+
let installArgv = argv;
|
|
224
|
+
if (verb === "install") {
|
|
225
|
+
const { values, positionals } = parseArgs({
|
|
226
|
+
args: argv,
|
|
227
|
+
options: {
|
|
228
|
+
workspace: { type: "string" },
|
|
229
|
+
repo: { type: "string" },
|
|
230
|
+
},
|
|
231
|
+
allowPositionals: true,
|
|
232
|
+
});
|
|
233
|
+
installArgv = positionals;
|
|
234
|
+
const declaredWorkspace = values.workspace?.trim();
|
|
235
|
+
const declaredRepo = values.repo?.trim();
|
|
236
|
+
if (declaredWorkspace || declaredRepo) {
|
|
237
|
+
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
238
|
+
writeOpsEnvFile(envPath, {
|
|
239
|
+
...(declaredWorkspace ? { CONDUIT_WORKSPACE: declaredWorkspace } : {}),
|
|
240
|
+
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
241
|
+
});
|
|
242
|
+
console.log(`Saved ${[
|
|
243
|
+
declaredWorkspace ? `workspace ${declaredWorkspace}` : "",
|
|
244
|
+
declaredRepo ? `repo ${declaredRepo}` : "",
|
|
245
|
+
].filter(Boolean).join(" and ")} to ${envPath}`);
|
|
246
|
+
installEnv = {
|
|
247
|
+
...env,
|
|
248
|
+
...(declaredWorkspace ? { CONDUIT_WORKSPACE: declaredWorkspace } : {}),
|
|
249
|
+
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
250
|
+
loadedFrom: env.loadedFrom ?? envPath,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
requireOpsEnv(verb === "install" ? installEnv : env);
|
|
210
255
|
if (verb === "switch") {
|
|
211
256
|
const { values } = parseArgs({
|
|
212
257
|
args: argv,
|
|
@@ -278,10 +323,11 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
278
323
|
return;
|
|
279
324
|
}
|
|
280
325
|
if (verb === "install") {
|
|
281
|
-
if (!
|
|
282
|
-
throw new Error(`
|
|
283
|
-
|
|
284
|
-
const
|
|
326
|
+
if (!installEnv.CONDUIT_WORKSPACE) {
|
|
327
|
+
throw new Error(`No workspace declared. Rerun with --workspace /path/to/repo, or set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
328
|
+
}
|
|
329
|
+
const workspace = resolve(expandOpsValue(installEnv.CONDUIT_WORKSPACE));
|
|
330
|
+
const drivers = await resolveDrivers(installEnv, installArgv);
|
|
285
331
|
for (const id of drivers) {
|
|
286
332
|
if (LOCAL_FUEL_DRIVERS.has(id))
|
|
287
333
|
runBridge(["drivers", "fuel", id, "local"]);
|
|
@@ -291,8 +337,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
291
337
|
runBridge(["ops", "doctor"]);
|
|
292
338
|
if (host === "win32") {
|
|
293
339
|
const runnerArgs = ["runner", "--workspace", workspace];
|
|
294
|
-
if (
|
|
295
|
-
runnerArgs.push("--ensure-checkout",
|
|
340
|
+
if (installEnv.CONDUIT_REPO)
|
|
341
|
+
runnerArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
296
342
|
console.log("Windows: background install-service is not available.");
|
|
297
343
|
console.log("Keep a terminal open and run:");
|
|
298
344
|
console.log(` npx @miraland-labs/conduit-bridge@latest ${shellQuoteArgs(runnerArgs)}`);
|
|
@@ -300,8 +346,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
300
346
|
return;
|
|
301
347
|
}
|
|
302
348
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
303
|
-
if (
|
|
304
|
-
installArgs.push("--ensure-checkout",
|
|
349
|
+
if (installEnv.CONDUIT_REPO)
|
|
350
|
+
installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
305
351
|
console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
|
|
306
352
|
runBridge(installArgs);
|
|
307
353
|
console.log("Done. Check with: npx @miraland-labs/conduit-bridge@latest ops status");
|
package/dist/service.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir, platform } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
export const SERVICE_LABEL = "io.miraland.conduit-runner";
|
|
7
7
|
function configDir() {
|
|
8
8
|
return join(homedir(), ".config", "conduit");
|
|
@@ -118,6 +118,22 @@ function run(command, args) {
|
|
|
118
118
|
throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
function shellQuote(value) {
|
|
122
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `launchctl bootout` of a loaded service kills that service's whole process group — including
|
|
126
|
+
* this process when install-service runs from inside the runner it replaces (Connect on-shift
|
|
127
|
+
* auto-apply). Observed in the field (2026-08-05): the runner died at bootout, the bootstrap on
|
|
128
|
+
* the next line never ran, and the machine stayed Binding incomplete with the service unloaded.
|
|
129
|
+
* The swap therefore runs in a detached helper that survives the bootout; failures append to the
|
|
130
|
+
* runner log.
|
|
131
|
+
*/
|
|
132
|
+
export function launchdSwapCommand(domain, plistPath, log) {
|
|
133
|
+
const plist = shellQuote(plistPath);
|
|
134
|
+
const logQ = shellQuote(log);
|
|
135
|
+
return `sleep 1; launchctl bootout ${domain}/${SERVICE_LABEL} 2>>${logQ}; launchctl bootstrap ${domain} ${plist} 2>>${logQ} || launchctl load -w ${plist} 2>>${logQ}`;
|
|
136
|
+
}
|
|
121
137
|
export async function installRunnerService(options = {}) {
|
|
122
138
|
const host = platform();
|
|
123
139
|
if (host !== "darwin" && host !== "linux") {
|
|
@@ -129,12 +145,19 @@ export async function installRunnerService(options = {}) {
|
|
|
129
145
|
await mkdir(dirname(plistPath), { recursive: true });
|
|
130
146
|
await mkdir(dirname(logPath()), { recursive: true });
|
|
131
147
|
await writeFile(plistPath, launchdPlist(programArguments, logPath()), { mode: 0o644 });
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
// Detached helper (bootout → bootstrap → load fallback): see launchdSwapCommand for why the
|
|
149
|
+
// swap must survive this process dying at bootout.
|
|
150
|
+
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
151
|
+
spawn("/bin/sh", ["-c", launchdSwapCommand(domain, plistPath, logPath())], { detached: true, stdio: "ignore" }).unref();
|
|
152
|
+
// Confirm the reloaded service when this process survives to see it (an interactive install).
|
|
153
|
+
// A self-replacing runner dies at the helper's bootout and the helper still finishes the swap.
|
|
154
|
+
let loaded = false;
|
|
155
|
+
for (let attempt = 0; attempt < 16 && !loaded; attempt++) {
|
|
156
|
+
await new Promise((resolveSleep) => setTimeout(resolveSleep, attempt === 0 ? 2000 : 500));
|
|
157
|
+
loaded = spawnSync("launchctl", ["print", `${domain}/${SERVICE_LABEL}`], { encoding: "utf8" }).status === 0;
|
|
137
158
|
}
|
|
159
|
+
if (!loaded)
|
|
160
|
+
throw new Error(`launchd did not load ${SERVICE_LABEL}; see ${logPath()}`);
|
|
138
161
|
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "darwin", programArguments, options, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
139
162
|
return { path: plistPath, platform: "darwin" };
|
|
140
163
|
}
|
package/ops/env.example
CHANGED
|
@@ -11,6 +11,8 @@ CONDUIT_URL=https://api.conduit.miraland.io
|
|
|
11
11
|
# CONDUIT_ORG=your-org-slug
|
|
12
12
|
CONDUIT_WORKSPACE=$HOME/path/to/your-repo
|
|
13
13
|
# CONDUIT_REPO=https://github.com/your-org/your-repo.git
|
|
14
|
-
CONDUIT_DRIVERS
|
|
14
|
+
# Leave CONDUIT_DRIVERS unset to auto-detect the coding agents installed on this computer.
|
|
15
|
+
# Pin explicitly only when you want a subset, e.g.:
|
|
16
|
+
# CONDUIT_DRIVERS=claude-code codex
|
|
15
17
|
# Product-standard roles — leave all three so Start/retry plans can match without reconnect.
|
|
16
18
|
CONDUIT_ROLES=implement research review
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|