xmux-bridge 1.0.39

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.
@@ -0,0 +1,799 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const { spawnSync } = require("node:child_process");
8
+
9
+ const SERVER_NAME = "xmux_lead";
10
+ const MARKETPLACE_NAME = "xmux-local";
11
+ const PLUGIN_KEY = `xmux@${MARKETPLACE_NAME}`;
12
+ const RULE_BEGIN = "# XMUX_COMMAND_RULE_BEGIN";
13
+ const RULE_END = "# XMUX_COMMAND_RULE_END";
14
+ const LEGACY_PREFIX = "a" + "mux";
15
+ const LEGACY_SERVER_NAMES = [`${LEGACY_PREFIX}_lead`];
16
+ const LEGACY_MARKETPLACE_NAMES = [`${LEGACY_PREFIX}-local`];
17
+ const LEGACY_PLUGIN_KEYS = [`${LEGACY_PREFIX}@${LEGACY_PREFIX}-local`];
18
+ const LOCAL_PLUGIN_CACHE_VERSION = "local";
19
+ const SKILL_MARKER = ".xmux-managed-skill";
20
+ const DEFAULT_MCP_PACKAGE = "xmux-bridge";
21
+ const DEFAULT_MCP_BIN = "xmux-lead-mcp";
22
+
23
+ function expandUser(value) {
24
+ const text = String(value || "");
25
+ if (text === "~") return os.homedir();
26
+ if (text.startsWith("~/")) return path.join(os.homedir(), text.slice(2));
27
+ return text;
28
+ }
29
+
30
+ function abs(value) {
31
+ return path.resolve(expandUser(value));
32
+ }
33
+
34
+ function stable_homebrew_xmux_install_dir(xmuxInstallDir) {
35
+ const installDir = abs(xmuxInstallDir);
36
+ const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
37
+ if (!installDir.includes(marker) || !installDir.endsWith(`${path.sep}libexec`)) {
38
+ return installDir;
39
+ }
40
+ if (!fs.existsSync(path.join(installDir, "xmux.zsh"))) {
41
+ return installDir;
42
+ }
43
+ const prefix = installDir.split(marker, 1)[0];
44
+ const candidate = path.join(prefix, "opt", "xmux", "libexec");
45
+ return fs.existsSync(path.join(candidate, "xmux.zsh")) ? candidate : installDir;
46
+ }
47
+
48
+ function stable_homebrew_xmux_file_path(inputPath) {
49
+ const resolved = abs(inputPath);
50
+ const installDir = path.dirname(resolved);
51
+ const stableInstallDir = stable_homebrew_xmux_install_dir(installDir);
52
+ if (stableInstallDir === installDir) return resolved;
53
+ const candidate = path.join(stableInstallDir, path.basename(resolved));
54
+ return fs.existsSync(candidate) ? candidate : resolved;
55
+ }
56
+
57
+ function resolve_path_with_node() {
58
+ const nodeBinDir = path.dirname(fs.realpathSync(process.execPath));
59
+ const baseDirs = [nodeBinDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
60
+ return [...new Set(baseDirs)].join(":");
61
+ }
62
+
63
+ function read_text(filePath) {
64
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
65
+ }
66
+
67
+ function write_text(filePath, content) {
68
+ fs.mkdirSync(path.dirname(filePath) || ".", { recursive: true });
69
+ fs.writeFileSync(filePath, content, "utf8");
70
+ }
71
+
72
+ function remove_toml_blocks(content, matcher) {
73
+ const lines = content.split("\n");
74
+ const out = [];
75
+ let skip = false;
76
+ for (const line of lines) {
77
+ const stripped = line.trim();
78
+ if (matcher(stripped)) {
79
+ skip = true;
80
+ continue;
81
+ }
82
+ if (skip && stripped.startsWith("[") && !matcher(stripped)) {
83
+ skip = false;
84
+ }
85
+ if (!skip) out.push(line);
86
+ }
87
+ while (out.length && out[out.length - 1].trim() === "") out.pop();
88
+ return out.join("\n");
89
+ }
90
+
91
+ function remove_xmux_blocks(content) {
92
+ for (const name of [SERVER_NAME, ...LEGACY_SERVER_NAMES]) {
93
+ content = remove_toml_blocks(content, (stripped) => stripped.startsWith(`[mcp_servers.${name}`));
94
+ }
95
+ for (const name of [MARKETPLACE_NAME, ...LEGACY_MARKETPLACE_NAMES]) {
96
+ content = remove_toml_blocks(content, (stripped) => stripped === `[marketplaces.${name}]`);
97
+ }
98
+ for (const key of [PLUGIN_KEY, ...LEGACY_PLUGIN_KEYS]) {
99
+ content = remove_toml_blocks(content, (stripped) => stripped === `[plugins."${key}"]`);
100
+ }
101
+ return content;
102
+ }
103
+
104
+ function remove_marker_block(content, begin, end) {
105
+ const lines = content.split("\n");
106
+ const out = [];
107
+ let skip = false;
108
+ for (const line of lines) {
109
+ const stripped = line.trim();
110
+ if (stripped === begin) {
111
+ skip = true;
112
+ continue;
113
+ }
114
+ if (skip && stripped === end) {
115
+ skip = false;
116
+ continue;
117
+ }
118
+ if (!skip) out.push(line);
119
+ }
120
+ while (out.length && out[out.length - 1].trim() === "") out.pop();
121
+ return out.join("\n");
122
+ }
123
+
124
+ function read_json(filePath) {
125
+ if (!fs.existsSync(filePath)) return null;
126
+ try {
127
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
128
+ } catch (_) {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function package_spec_has_version(packageSpec) {
134
+ const text = String(packageSpec || "");
135
+ if (!text) return false;
136
+ if (text.startsWith("@")) return text.indexOf("@", 1) !== -1;
137
+ return text.includes("@");
138
+ }
139
+
140
+ function xmux_version_from_install_dir(xmuxInstallDir) {
141
+ const content = read_text(path.join(abs(xmuxInstallDir), "xmux.zsh"));
142
+ const match = content.match(/^XMUX_VERSION=["']([^"']+)["']/m);
143
+ return match ? match[1] : "";
144
+ }
145
+
146
+ function default_mcp_package_spec(xmuxInstallDir, packageName = "", packageVersion = "") {
147
+ const installPackage = read_json(path.join(abs(xmuxInstallDir), "package.json")) || {};
148
+ const scriptPackage = read_json(path.join(path.dirname(path.dirname(abs(__filename))), "package.json")) || {};
149
+ const name = packageName
150
+ || process.env.XMUX_MCP_NPM_PACKAGE
151
+ || installPackage.name
152
+ || scriptPackage.name
153
+ || DEFAULT_MCP_PACKAGE;
154
+ const version = packageVersion
155
+ || process.env.XMUX_MCP_NPM_VERSION
156
+ || installPackage.version
157
+ || xmux_version_from_install_dir(xmuxInstallDir)
158
+ || scriptPackage.version
159
+ || "";
160
+ if (!version || package_spec_has_version(name)) return name;
161
+ return `${name}@${version}`;
162
+ }
163
+
164
+ function node_mcp_config(serverPath) {
165
+ const normalized = stable_homebrew_xmux_file_path(serverPath);
166
+ return {
167
+ mode: "node",
168
+ command: "node",
169
+ args: [normalized],
170
+ server_path: normalized,
171
+ label: normalized,
172
+ };
173
+ }
174
+
175
+ function npx_mcp_config(packageSpec, binName = DEFAULT_MCP_BIN) {
176
+ return {
177
+ mode: "npx",
178
+ command: "npx",
179
+ args: ["-y", "-p", packageSpec, binName],
180
+ package_spec: packageSpec,
181
+ bin: binName,
182
+ label: `npx -y -p ${packageSpec} ${binName}`,
183
+ };
184
+ }
185
+
186
+ function resolve_mcp_config(xmuxInstallDir, opts = {}) {
187
+ if (opts.server_path) return node_mcp_config(opts.server_path);
188
+ const packageSpec = default_mcp_package_spec(xmuxInstallDir, opts.mcp_package, opts.mcp_version);
189
+ return npx_mcp_config(packageSpec, opts.mcp_bin || DEFAULT_MCP_BIN);
190
+ }
191
+
192
+ function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts = {}) {
193
+ if (mcpConfigOrServerPath && typeof mcpConfigOrServerPath === "object") {
194
+ return {
195
+ mode: mcpConfigOrServerPath.mode || "custom",
196
+ command: mcpConfigOrServerPath.command,
197
+ args: [...(mcpConfigOrServerPath.args || [])],
198
+ server_path: mcpConfigOrServerPath.server_path || "",
199
+ package_spec: mcpConfigOrServerPath.package_spec || "",
200
+ bin: mcpConfigOrServerPath.bin || "",
201
+ label: mcpConfigOrServerPath.label || [
202
+ mcpConfigOrServerPath.command,
203
+ ...(mcpConfigOrServerPath.args || []),
204
+ ].join(" "),
205
+ };
206
+ }
207
+ if (mcpConfigOrServerPath) return node_mcp_config(mcpConfigOrServerPath);
208
+ return resolve_mcp_config(xmuxInstallDir, opts);
209
+ }
210
+
211
+ function mcp_args_toml(mcpConfig) {
212
+ return `args = [${mcpConfig.args.map(toml_quote).join(", ")}]`;
213
+ }
214
+
215
+ function build_block(mcpConfigOrServerPath, xmuxInstallDir) {
216
+ const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
217
+ const pathEnv = resolve_path_with_node();
218
+ const home = os.homedir();
219
+ return `[mcp_servers.${SERVER_NAME}]
220
+ command = ${toml_quote(mcpConfig.command)}
221
+ ${mcp_args_toml(mcpConfig)}
222
+ startup_timeout_sec = 10
223
+ tool_timeout_sec = 300
224
+
225
+ [mcp_servers.${SERVER_NAME}.env]
226
+ PATH = "${pathEnv}"
227
+ HOME = "${home}"
228
+ XMUX_INSTALL_DIR = "${xmuxInstallDir}"
229
+ `;
230
+ }
231
+
232
+ function toml_quote(value) {
233
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
234
+ }
235
+
236
+ function parse_toml_assignment_value(line, key) {
237
+ const match = line.match(new RegExp(`^\\s*${key}\\s*=\\s*(.*)\\s*$`));
238
+ if (!match) return null;
239
+ const raw = match[1].trim();
240
+ if (raw.startsWith('"') && raw.endsWith('"')) {
241
+ try {
242
+ return JSON.parse(raw);
243
+ } catch (_) {
244
+ return raw.slice(1, -1);
245
+ }
246
+ }
247
+ return null;
248
+ }
249
+
250
+ function is_xmux_runtime_bin_path(candidatePath, currentXmuxBin) {
251
+ const expanded = abs(candidatePath);
252
+ if (expanded === abs(currentXmuxBin)) return true;
253
+ if (path.basename(expanded) !== "bin") return false;
254
+ const installDir = path.dirname(expanded);
255
+ if (fs.existsSync(path.join(installDir, "xmux.zsh")) && fs.existsSync(path.join(expanded, "xmux"))) {
256
+ return true;
257
+ }
258
+ if (path.basename(installDir) !== "libexec") return false;
259
+ const packageDir = path.dirname(installDir);
260
+ const parentDir = path.dirname(packageDir);
261
+ return path.basename(packageDir) === "xmux" || path.basename(parentDir) === "xmux";
262
+ }
263
+
264
+ function path_with_xmux_bin(xmuxInstallDir, basePath = null) {
265
+ const xmuxBin = path.join(abs(xmuxInstallDir), "bin");
266
+ const source = basePath == null ? resolve_path_with_node() : basePath;
267
+ const parts = source.split(":").filter((part) => part && !is_xmux_runtime_bin_path(part, xmuxBin));
268
+ return [xmuxBin, ...parts].join(":");
269
+ }
270
+
271
+ function ensure_codex_shell_environment(content, xmuxInstallDir) {
272
+ const installDir = abs(xmuxInstallDir);
273
+ const lines = content.split("\n");
274
+ const header = "[shell_environment_policy.set]";
275
+ let start = lines.findIndex((line) => line.trim() === header);
276
+ if (start < 0) {
277
+ const block = [
278
+ header,
279
+ `PATH = ${toml_quote(path_with_xmux_bin(installDir))}`,
280
+ `XMUX_INSTALL_DIR = ${toml_quote(installDir)}`,
281
+ ].join("\n");
282
+ return content.trim() ? `${content.trimEnd()}\n\n${block}\n` : `${block}\n`;
283
+ }
284
+
285
+ let end = lines.length;
286
+ for (let i = start + 1; i < lines.length; i += 1) {
287
+ const stripped = lines[i].trim();
288
+ if (stripped.startsWith("[") && stripped.endsWith("]")) {
289
+ end = i;
290
+ break;
291
+ }
292
+ }
293
+
294
+ let seenPath = false;
295
+ let seenInstall = false;
296
+ for (let i = start + 1; i < end; i += 1) {
297
+ const stripped = lines[i].trim();
298
+ const key = stripped.includes("=") ? stripped.split("=", 1)[0].trim() : "";
299
+ if (key === "PATH") {
300
+ const current = parse_toml_assignment_value(stripped, "PATH");
301
+ const base = current == null ? resolve_path_with_node() : current;
302
+ lines[i] = `PATH = ${toml_quote(path_with_xmux_bin(installDir, base))}`;
303
+ seenPath = true;
304
+ } else if (key === "XMUX_INSTALL_DIR") {
305
+ lines[i] = `XMUX_INSTALL_DIR = ${toml_quote(installDir)}`;
306
+ seenInstall = true;
307
+ }
308
+ }
309
+ const inserts = [];
310
+ if (!seenPath) inserts.push(`PATH = ${toml_quote(path_with_xmux_bin(installDir))}`);
311
+ if (!seenInstall) inserts.push(`XMUX_INSTALL_DIR = ${toml_quote(installDir)}`);
312
+ if (inserts.length) lines.splice(start + 1, 0, ...inserts);
313
+ return `${lines.join("\n").trimEnd()}\n`;
314
+ }
315
+
316
+ function remove_codex_shell_environment(content, xmuxInstallDir) {
317
+ const lines = content.split("\n");
318
+ const header = "[shell_environment_policy.set]";
319
+ const installBin = path.join(abs(xmuxInstallDir), "bin");
320
+ const start = lines.findIndex((line) => line.trim() === header);
321
+ if (start < 0) return content;
322
+
323
+ let end = lines.length;
324
+ for (let i = start + 1; i < lines.length; i += 1) {
325
+ const stripped = lines[i].trim();
326
+ if (stripped.startsWith("[") && stripped.endsWith("]")) {
327
+ end = i;
328
+ break;
329
+ }
330
+ }
331
+
332
+ const sectionLines = [];
333
+ for (const line of lines.slice(start + 1, end)) {
334
+ const stripped = line.trim();
335
+ const key = stripped.includes("=") ? stripped.split("=", 1)[0].trim() : "";
336
+ if (key === "XMUX_INSTALL_DIR") continue;
337
+ if (key === "PATH") {
338
+ const current = parse_toml_assignment_value(stripped, "PATH");
339
+ if (current != null) {
340
+ const parts = current.split(":").filter((part) => part && !is_xmux_runtime_bin_path(part, installBin));
341
+ if (parts.length) sectionLines.push(`PATH = ${toml_quote(parts.join(":"))}`);
342
+ continue;
343
+ }
344
+ }
345
+ sectionLines.push(line);
346
+ }
347
+
348
+ if (sectionLines.some((line) => line.trim())) {
349
+ lines.splice(start + 1, end - start - 1, ...sectionLines);
350
+ } else {
351
+ lines.splice(start, end - start);
352
+ }
353
+ while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
354
+ return lines.length ? `${lines.join("\n")}\n` : "";
355
+ }
356
+
357
+ function codex_home(configPath) {
358
+ return path.dirname(abs(configPath));
359
+ }
360
+
361
+ function plugin_cache_root(configPath) {
362
+ return path.join(codex_home(configPath), "plugins", "cache", MARKETPLACE_NAME, "xmux");
363
+ }
364
+
365
+ function plugin_cache_path(configPath) {
366
+ return path.join(plugin_cache_root(configPath), LOCAL_PLUGIN_CACHE_VERSION);
367
+ }
368
+
369
+ function legacy_plugin_cache_roots(configPath) {
370
+ return LEGACY_MARKETPLACE_NAMES.map((marketplace) => (
371
+ path.join(codex_home(configPath), "plugins", "cache", marketplace, LEGACY_PREFIX)
372
+ ));
373
+ }
374
+
375
+ function remove_local_plugin_cache(configPath) {
376
+ const home = codex_home(configPath);
377
+ for (const cachePath of [plugin_cache_root(configPath), ...legacy_plugin_cache_roots(configPath)]) {
378
+ if (fs.existsSync(cachePath)) {
379
+ fs.rmSync(cachePath, { recursive: true, force: true });
380
+ }
381
+ let parent = path.dirname(cachePath);
382
+ while (abs(parent) !== home && abs(parent).startsWith(home)) {
383
+ try {
384
+ fs.rmdirSync(parent);
385
+ } catch (_) {
386
+ break;
387
+ }
388
+ parent = path.dirname(parent);
389
+ }
390
+ }
391
+ }
392
+
393
+ function rules_path(configPath) {
394
+ return path.join(codex_home(configPath), "rules", "default.rules");
395
+ }
396
+
397
+ function install_xmux_command_rule(configPath) {
398
+ const filePath = rules_path(configPath);
399
+ let content = remove_marker_block(read_text(filePath), RULE_BEGIN, RULE_END);
400
+ const block = [
401
+ RULE_BEGIN,
402
+ "# Allow the scoped XMux wrapper command; XMux skills still control operation scope.",
403
+ 'prefix_rule(pattern=["xmux"], decision="allow")',
404
+ RULE_END,
405
+ ].join("\n");
406
+ content = content.trim() ? `${content.trimEnd()}\n\n${block}\n` : `${block}\n`;
407
+ write_text(filePath, content);
408
+ return null;
409
+ }
410
+
411
+ function remove_xmux_command_rule(configPath) {
412
+ const filePath = rules_path(configPath);
413
+ const content = remove_marker_block(read_text(filePath), RULE_BEGIN, RULE_END);
414
+ write_text(filePath, content ? `${content}\n` : "");
415
+ return null;
416
+ }
417
+
418
+ function skills_root(configPath) {
419
+ return path.join(codex_home(configPath), "skills");
420
+ }
421
+
422
+ function skill_source_dirs(xmuxInstallDir, skillsDir = "") {
423
+ const candidates = [];
424
+ if (skillsDir) candidates.push(expandUser(skillsDir));
425
+ if (process.env.XMUX_CODEX_SKILLS_DIR) candidates.push(expandUser(process.env.XMUX_CODEX_SKILLS_DIR));
426
+ const seen = new Set();
427
+ const out = [];
428
+ for (const candidate of candidates) {
429
+ const resolved = abs(candidate);
430
+ if (!seen.has(resolved)) {
431
+ seen.add(resolved);
432
+ out.push(resolved);
433
+ }
434
+ }
435
+ return out;
436
+ }
437
+
438
+ function xmux_skill_sources(xmuxInstallDir, skillsDir = "") {
439
+ const sources = new Map();
440
+ for (const base of skill_source_dirs(xmuxInstallDir, skillsDir)) {
441
+ if (!fs.existsSync(base) || !fs.statSync(base).isDirectory()) continue;
442
+ for (const name of fs.readdirSync(base).sort()) {
443
+ if (!name.startsWith("xmux-") || sources.has(name)) continue;
444
+ const source = path.join(base, name);
445
+ if (fs.existsSync(path.join(source, "SKILL.md"))) sources.set(name, source);
446
+ }
447
+ }
448
+ return [...sources.entries()].sort(([a], [b]) => a.localeCompare(b));
449
+ }
450
+
451
+ function is_xmux_managed_skill(candidatePath) {
452
+ return fs.existsSync(candidatePath)
453
+ && fs.statSync(candidatePath).isDirectory()
454
+ && fs.existsSync(path.join(candidatePath, SKILL_MARKER));
455
+ }
456
+
457
+ function install_xmux_skills(configPath, xmuxInstallDir, skillsDir = "") {
458
+ const root = skills_root(configPath);
459
+ const installed = [];
460
+ for (const [name, source] of xmux_skill_sources(xmuxInstallDir, skillsDir)) {
461
+ const dst = path.join(root, name);
462
+ if (fs.existsSync(dst) && !is_xmux_managed_skill(dst)) continue;
463
+ fs.rmSync(dst, { recursive: true, force: true });
464
+ fs.mkdirSync(root, { recursive: true });
465
+ fs.cpSync(source, dst, { recursive: true });
466
+ write_text(path.join(dst, SKILL_MARKER), `${abs(source)}\n`);
467
+ installed.push(name);
468
+ }
469
+ return installed;
470
+ }
471
+
472
+ function remove_xmux_skills(configPath) {
473
+ const root = skills_root(configPath);
474
+ const removed = [];
475
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return removed;
476
+ for (const name of fs.readdirSync(root).sort()) {
477
+ if (!name.startsWith("xmux-")) continue;
478
+ const candidate = path.join(root, name);
479
+ if (!is_xmux_managed_skill(candidate)) continue;
480
+ fs.rmSync(candidate, { recursive: true, force: true });
481
+ removed.push(name);
482
+ }
483
+ return removed;
484
+ }
485
+
486
+ function _content_has_xmux_mcp(content, mcpConfigOrServerPath, xmuxInstallDir) {
487
+ const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
488
+ return content.includes(`[mcp_servers.${SERVER_NAME}]`)
489
+ && content.includes(`command = ${toml_quote(mcpConfig.command)}`)
490
+ && content.includes(mcp_args_toml(mcpConfig))
491
+ && content.includes(`XMUX_INSTALL_DIR = "${abs(xmuxInstallDir)}"`)
492
+ && !content.includes("XMUX_PROJECT_DIR =")
493
+ && !content.includes("XMUX_STATE_DIR =");
494
+ }
495
+
496
+ function _content_has_shell_environment(content, xmuxInstallDir) {
497
+ const installBin = path.join(abs(xmuxInstallDir), "bin");
498
+ return content.includes("[shell_environment_policy.set]")
499
+ && content.includes(`XMUX_INSTALL_DIR = "${abs(xmuxInstallDir)}"`)
500
+ && content.includes(installBin);
501
+ }
502
+
503
+ function _rules_have_xmux_command(configPath) {
504
+ const content = read_text(rules_path(configPath));
505
+ return content.includes(RULE_BEGIN)
506
+ && content.includes(RULE_END)
507
+ && content.includes('prefix_rule(pattern=["xmux"], decision="allow")');
508
+ }
509
+
510
+ function _installed_skill_names(configPath) {
511
+ const root = skills_root(configPath);
512
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return new Set();
513
+ return new Set(
514
+ fs.readdirSync(root)
515
+ .filter((name) => name.startsWith("xmux-"))
516
+ .filter((name) => fs.existsSync(path.join(root, name, "SKILL.md")))
517
+ .filter((name) => is_xmux_managed_skill(path.join(root, name))),
518
+ );
519
+ }
520
+
521
+ function splitShellWords(command) {
522
+ const words = [];
523
+ let current = "";
524
+ let quote = "";
525
+ for (let i = 0; i < command.length; i += 1) {
526
+ const ch = command[i];
527
+ if (quote) {
528
+ if (ch === quote) quote = "";
529
+ else current += ch;
530
+ continue;
531
+ }
532
+ if (ch === "'" || ch === '"') {
533
+ quote = ch;
534
+ } else if (/\s/.test(ch)) {
535
+ if (current) {
536
+ words.push(current);
537
+ current = "";
538
+ }
539
+ } else {
540
+ current += ch;
541
+ }
542
+ }
543
+ if (current) words.push(current);
544
+ return words;
545
+ }
546
+
547
+ function _xmux_lead_mcp_processes_from_ps(psOutput) {
548
+ const processes = [];
549
+ for (const rawLine of String(psOutput || "").split(/\r?\n/)) {
550
+ const stripped = rawLine.trim();
551
+ if (!stripped.includes("xmux-lead-mcp-server.js")) continue;
552
+ const match = stripped.match(/^(\d+)\s+(.*)$/);
553
+ if (!match) continue;
554
+ const [, pid, command] = match;
555
+ const tokens = splitShellWords(command.trim());
556
+ const serverPath = tokens.find((token) => token.endsWith("xmux-lead-mcp-server.js")) || "";
557
+ if (!serverPath) continue;
558
+ processes.push({ pid, command: command.trim(), server_path: abs(serverPath) });
559
+ }
560
+ return processes;
561
+ }
562
+
563
+ function running_xmux_lead_mcp_processes() {
564
+ if (process.env.XMUX_TEST_PS_OUTPUT !== undefined) {
565
+ return _xmux_lead_mcp_processes_from_ps(process.env.XMUX_TEST_PS_OUTPUT);
566
+ }
567
+ const result = spawnSync("ps", ["-Ao", "pid=,command="], { encoding: "utf8" });
568
+ if (result.status !== 0) return [];
569
+ return _xmux_lead_mcp_processes_from_ps(result.stdout);
570
+ }
571
+
572
+ function _is_homebrew_xmux_mcp_server(serverPath) {
573
+ const normalized = abs(serverPath);
574
+ return normalized.endsWith("xmux-lead-mcp-server.js")
575
+ && normalized.includes(`${path.sep}Cellar${path.sep}xmux${path.sep}`)
576
+ && normalized.includes(`${path.sep}libexec${path.sep}`);
577
+ }
578
+
579
+ function stale_xmux_lead_mcp_processes(expectedMcpConfigOrServerPath, processes = null) {
580
+ const expectedConfig = normalize_mcp_config(expectedMcpConfigOrServerPath);
581
+ if (expectedConfig.mode !== "node") return [];
582
+ const expected = abs(expectedConfig.server_path || expectedConfig.args[0] || "");
583
+ const source = processes || running_xmux_lead_mcp_processes();
584
+ const stale = [];
585
+ for (const proc of source) {
586
+ const serverPath = abs(proc.server_path || "");
587
+ if (!serverPath || serverPath === expected) continue;
588
+ if (!_is_homebrew_xmux_mcp_server(serverPath) && fs.existsSync(serverPath)) continue;
589
+ stale.push({ ...proc, server_path: serverPath });
590
+ }
591
+ return stale;
592
+ }
593
+
594
+ function doctor_codex(configPath, xmuxInstallDir, mcpConfigOrServerPath, skillsDir = "", quiet = false) {
595
+ const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
596
+ const content = read_text(configPath);
597
+ const issues = [];
598
+ const notes = [];
599
+
600
+ if (!fs.existsSync(configPath)) issues.push(`missing config: ${configPath}`);
601
+ else if (_content_has_xmux_mcp(content, mcpConfig, xmuxInstallDir)) notes.push(["OK", `mcp command points at ${mcpConfig.label}`]);
602
+ else issues.push("xmux_lead MCP config is missing or stale");
603
+
604
+ if (_content_has_shell_environment(content, xmuxInstallDir)) notes.push(["OK", "Codex shell PATH includes XMux bin"]);
605
+ else issues.push("Codex shell PATH/XMUX_INSTALL_DIR setup is missing or stale");
606
+
607
+ if (_rules_have_xmux_command(configPath)) notes.push(["OK", `scoped xmux command rule exists in ${rules_path(configPath)}`]);
608
+ else issues.push("scoped xmux command rule is missing");
609
+
610
+ const sourceNames = new Set(xmux_skill_sources(xmuxInstallDir, skillsDir).map(([name]) => name));
611
+ const installedNames = _installed_skill_names(configPath);
612
+ if (sourceNames.size) {
613
+ const missing = [...sourceNames].filter((name) => !installedNames.has(name)).sort();
614
+ if (missing.length) issues.push(`missing XMux Codex skills: ${missing.join(", ")}`);
615
+ else notes.push(["OK", `XMux Codex skills installed under ${skills_root(configPath)}`]);
616
+ } else if (installedNames.size) {
617
+ notes.push(["OK", `XMux Codex skills installed under ${skills_root(configPath)}`]);
618
+ } else {
619
+ notes.push(["WARN", "no XMux skill source directory found; pass --skills-dir or set XMUX_CODEX_SKILLS_DIR"]);
620
+ }
621
+
622
+ if (fs.existsSync(plugin_cache_path(configPath))) {
623
+ notes.push(["WARN", "legacy XMux plugin cache is present; run xmux setup-codex to remove it"]);
624
+ } else {
625
+ notes.push(["OK", "legacy XMux plugin cache is absent"]);
626
+ }
627
+
628
+ const staleProcesses = stale_xmux_lead_mcp_processes(mcpConfig);
629
+ for (const proc of staleProcesses.slice(0, 5)) {
630
+ notes.push([
631
+ "WARN",
632
+ `active xmux_lead MCP process pid ${proc.pid} uses ${proc.server_path}; restart that Codex/XMux session to load the configured server`,
633
+ ]);
634
+ }
635
+ if (staleProcesses.length > 5) {
636
+ notes.push(["WARN", `${staleProcesses.length - 5} more stale xmux_lead MCP process(es) detected`]);
637
+ }
638
+
639
+ if (quiet) return issues.length ? 1 : 0;
640
+ if (issues.length) {
641
+ console.log("[FAIL] XMux Codex setup is incomplete");
642
+ for (const issue of issues) console.log(` - ${issue}`);
643
+ for (const [level, note] of notes) console.log(` - [${level}] ${note}`);
644
+ console.log("Run: xmux setup-codex");
645
+ return 1;
646
+ }
647
+ console.log("[OK] XMux Codex setup looks ready");
648
+ for (const [level, note] of notes) console.log(` - [${level}] ${note}`);
649
+ return 0;
650
+ }
651
+
652
+ function parse_args(argv) {
653
+ const opts = {
654
+ remove: false,
655
+ doctor: false,
656
+ quiet: false,
657
+ install_skills: true,
658
+ skills_dir: "",
659
+ home: "",
660
+ project: "",
661
+ xmux_install_dir: "",
662
+ xmux_project_dir: "",
663
+ xmux_state_dir: "",
664
+ server_path: "",
665
+ mcp_package: "",
666
+ mcp_version: "",
667
+ mcp_bin: DEFAULT_MCP_BIN,
668
+ };
669
+ for (let i = 0; i < argv.length;) {
670
+ const arg = argv[i];
671
+ if (arg === "--remove") {
672
+ opts.remove = true; i += 1;
673
+ } else if (arg === "--doctor") {
674
+ opts.doctor = true; i += 1;
675
+ } else if (arg === "--quiet") {
676
+ opts.quiet = true; i += 1;
677
+ } else if (arg === "--without-skills") {
678
+ opts.install_skills = false; i += 1;
679
+ } else if ([
680
+ "--skills-dir",
681
+ "--home",
682
+ "--project",
683
+ "--xmux-install-dir",
684
+ "--xmux-project-dir",
685
+ "--xmux-state-dir",
686
+ "--server-path",
687
+ "--mcp-package",
688
+ "--mcp-version",
689
+ "--mcp-bin",
690
+ ].includes(arg) && i + 1 < argv.length) {
691
+ const key = arg.slice(2).replace(/-/g, "_");
692
+ opts[key] = expandUser(argv[i + 1]);
693
+ i += 2;
694
+ } else {
695
+ console.error(`unknown or incomplete argument: ${arg}`);
696
+ process.exit(2);
697
+ }
698
+ }
699
+ return opts;
700
+ }
701
+
702
+ function default_xmux_project_dir() {
703
+ let current = process.cwd();
704
+ while (current && current !== path.dirname(current)) {
705
+ if (fs.existsSync(path.join(current, ".git"))) return abs(current);
706
+ current = path.dirname(current);
707
+ }
708
+ return abs(process.cwd());
709
+ }
710
+
711
+ function default_xmux_state_dir(projectDir = null) {
712
+ return path.join(projectDir || default_xmux_project_dir(), ".codex", "xmux");
713
+ }
714
+
715
+ function resolve_config_path(opts) {
716
+ if (opts.home && opts.project) {
717
+ console.error("--home and --project are mutually exclusive");
718
+ process.exit(2);
719
+ }
720
+ if (opts.home) return path.join(expandUser(opts.home), "config.toml");
721
+ if (opts.project) return path.join(abs(opts.project), ".codex", "config.toml");
722
+ return path.join(os.homedir(), ".codex", "config.toml");
723
+ }
724
+
725
+ function main(argv = process.argv.slice(2)) {
726
+ const opts = parse_args(argv);
727
+ const configPath = resolve_config_path(opts);
728
+ const scriptInstallDir = path.dirname(path.dirname(abs(__filename)));
729
+ const rawInstallDir = abs(opts.xmux_install_dir || scriptInstallDir);
730
+ const xmuxInstallDir = stable_homebrew_xmux_install_dir(rawInstallDir);
731
+ const xmuxProjectDir = abs(opts.xmux_project_dir || default_xmux_project_dir());
732
+ const xmuxStateDir = abs(opts.xmux_state_dir || default_xmux_state_dir(xmuxProjectDir));
733
+ const mcpConfig = resolve_mcp_config(xmuxInstallDir, opts);
734
+
735
+ if (opts.doctor) {
736
+ return doctor_codex(configPath, xmuxInstallDir, mcpConfig, opts.skills_dir, opts.quiet);
737
+ }
738
+
739
+ let content = remove_xmux_blocks(read_text(configPath));
740
+ if (opts.remove) {
741
+ content = remove_codex_shell_environment(content, xmuxInstallDir);
742
+ remove_local_plugin_cache(configPath);
743
+ remove_xmux_command_rule(configPath);
744
+ const removedSkills = remove_xmux_skills(configPath);
745
+ write_text(configPath, content ? `${content}` : "");
746
+ console.log(`[OK] Removed XMux Codex lead config from ${configPath}`);
747
+ if (removedSkills.length) console.log(` removed skills: ${removedSkills.join(", ")}`);
748
+ return 0;
749
+ }
750
+
751
+ const globalConfig = path.join(os.homedir(), ".codex", "config.toml");
752
+ if (opts.project && !content.trim() && abs(globalConfig) !== abs(configPath)) {
753
+ content = remove_xmux_blocks(read_text(globalConfig));
754
+ }
755
+
756
+ content = ensure_codex_shell_environment(content, xmuxInstallDir);
757
+ const block = build_block(mcpConfig, xmuxInstallDir, xmuxProjectDir, xmuxStateDir);
758
+ if (content && !content.endsWith("\n")) content += "\n";
759
+ const newContent = content.trim() ? `${content}\n${block}` : block;
760
+ write_text(configPath, newContent);
761
+ remove_local_plugin_cache(configPath);
762
+ const installedSkills = opts.install_skills
763
+ ? install_xmux_skills(configPath, xmuxInstallDir, opts.skills_dir)
764
+ : [];
765
+ install_xmux_command_rule(configPath);
766
+
767
+ console.log(`[OK] Wrote ${SERVER_NAME} to ${configPath}`);
768
+ console.log(` mcp: ${mcpConfig.label}`);
769
+ console.log(` xmux_install_dir: ${xmuxInstallDir}`);
770
+ console.log(" xmux_project_dir: inherited from xmux-launched Codex runtime");
771
+ console.log(" xmux_state_dir: inherited from xmux-launched Codex runtime");
772
+ if (installedSkills.length) console.log(` skills: ${installedSkills.join(", ")}`);
773
+ else if (opts.install_skills) console.log(" skills: skipped; pass --skills-dir or set XMUX_CODEX_SKILLS_DIR");
774
+ console.log(" plugin_cache: disabled; stale XMux plugin cache removed if present");
775
+ return 0;
776
+ }
777
+
778
+ if (require.main === module) {
779
+ try {
780
+ process.exitCode = main();
781
+ } catch (error) {
782
+ console.error(error && error.stack ? error.stack : String(error));
783
+ process.exitCode = 1;
784
+ }
785
+ }
786
+
787
+ module.exports = {
788
+ remove_xmux_blocks,
789
+ build_block,
790
+ default_mcp_package_spec,
791
+ resolve_mcp_config,
792
+ path_with_xmux_bin,
793
+ ensure_codex_shell_environment,
794
+ install_xmux_command_rule,
795
+ remove_xmux_command_rule,
796
+ _xmux_lead_mcp_processes_from_ps,
797
+ stale_xmux_lead_mcp_processes,
798
+ main,
799
+ };