xmux-bridge 1.0.39 → 1.0.40
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 +4 -5
- package/bridge-mcp-server.js +84 -21
- package/package.json +1 -1
- package/scripts/setup_xmux_codex_mcp.js +30 -4
- package/xmux-lead-mcp-server.js +78 -20
- package/xmux.zsh +5 -5
package/README.md
CHANGED
|
@@ -166,8 +166,9 @@ The Codex lead MCP server is `xmux_lead`. `xmux setup-codex` configures it so
|
|
|
166
166
|
Codex can route requests, wait for teammate responses, read events, and inspect
|
|
167
167
|
team status.
|
|
168
168
|
The global MCP config is install-scoped: the MCP command is a versioned npm
|
|
169
|
-
entrypoint
|
|
170
|
-
|
|
169
|
+
entrypoint launched with a project-neutral `npx --prefix`, while
|
|
170
|
+
`XMUX_INSTALL_DIR` points at the Homebrew runtime that owns wrapper scripts,
|
|
171
|
+
state discovery, and lifecycle. It does not pin
|
|
171
172
|
`XMUX_PROJECT_DIR`/`XMUX_STATE_DIR`; those values come from the active
|
|
172
173
|
`xmux -n <session>` lead runtime.
|
|
173
174
|
|
|
@@ -198,15 +199,13 @@ $xmux-send-pane
|
|
|
198
199
|
Development verification for agent/runtime changes:
|
|
199
200
|
|
|
200
201
|
```bash
|
|
201
|
-
pytest tests -q
|
|
202
202
|
zsh -n xmux.zsh
|
|
203
203
|
zsh -n xmux-bridge.zsh
|
|
204
204
|
node --check scripts/setup_xmux_codex_mcp.js
|
|
205
205
|
git diff --check
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
[Homebrew distribution](docs/operations/homebrew.md).
|
|
208
|
+
Homebrew distribution notes live in [Homebrew distribution](docs/operations/homebrew.md).
|
|
210
209
|
|
|
211
210
|
## Docs
|
|
212
211
|
|
package/bridge-mcp-server.js
CHANGED
|
@@ -305,36 +305,99 @@ function buildResponse(msg) {
|
|
|
305
305
|
|
|
306
306
|
// ── STDIO mode ────────────────────────────────────────────────────────────────
|
|
307
307
|
// CRITICAL: register stdin listener FIRST to avoid race condition.
|
|
308
|
-
// Provider CLIs send `initialize` immediately after spawn; if
|
|
308
|
+
// Provider CLIs send `initialize` immediately after spawn; if stdin isn't
|
|
309
309
|
// listening yet the message is lost and the CLI times out (Tools: none).
|
|
310
310
|
|
|
311
|
+
function writeMcpResponse(resp, framed) {
|
|
312
|
+
const body = JSON.stringify(resp);
|
|
313
|
+
if (framed) {
|
|
314
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
|
|
315
|
+
} else {
|
|
316
|
+
process.stdout.write(body + '\n');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleStdioMessage(msg, framed) {
|
|
321
|
+
const resp = buildResponse(msg);
|
|
322
|
+
if (resp) writeMcpResponse(resp, framed);
|
|
323
|
+
}
|
|
324
|
+
|
|
311
325
|
function startStdio() {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
326
|
+
let buffer = Buffer.alloc(0);
|
|
327
|
+
|
|
328
|
+
function parseContentLengthHeader(header) {
|
|
329
|
+
for (const line of header.split(/\r?\n/)) {
|
|
330
|
+
const match = line.match(/^Content-Length:\s*(\d+)\s*$/i);
|
|
331
|
+
if (match) return Number.parseInt(match[1], 10);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
315
335
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (!line.trim()) return;
|
|
336
|
+
function handleJsonPayload(payload, framed) {
|
|
337
|
+
if (!payload.trim()) return;
|
|
319
338
|
let msg;
|
|
320
|
-
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} else {
|
|
325
|
-
messageQueue.push(msg);
|
|
339
|
+
try {
|
|
340
|
+
msg = JSON.parse(payload);
|
|
341
|
+
} catch (_) {
|
|
342
|
+
return;
|
|
326
343
|
}
|
|
344
|
+
handleStdioMessage(msg, framed);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function drain(flush = false) {
|
|
348
|
+
while (buffer.length) {
|
|
349
|
+
while (buffer[0] === 0x0a || buffer[0] === 0x0d) buffer = buffer.subarray(1);
|
|
350
|
+
if (!buffer.length) return;
|
|
351
|
+
|
|
352
|
+
const text = buffer.toString('utf8');
|
|
353
|
+
if (text.toLowerCase().startsWith('content-length:')) {
|
|
354
|
+
let headerEnd = text.indexOf('\r\n\r\n');
|
|
355
|
+
let separatorLength = 4;
|
|
356
|
+
if (headerEnd === -1) {
|
|
357
|
+
headerEnd = text.indexOf('\n\n');
|
|
358
|
+
separatorLength = 2;
|
|
359
|
+
}
|
|
360
|
+
if (headerEnd === -1) return;
|
|
361
|
+
|
|
362
|
+
const header = text.slice(0, headerEnd);
|
|
363
|
+
const contentLength = parseContentLengthHeader(header);
|
|
364
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
365
|
+
buffer = Buffer.alloc(0);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + separatorLength), 'utf8');
|
|
370
|
+
if (buffer.length < bodyStart + contentLength) return;
|
|
371
|
+
const body = buffer.subarray(bodyStart, bodyStart + contentLength).toString('utf8');
|
|
372
|
+
buffer = buffer.subarray(bodyStart + contentLength);
|
|
373
|
+
handleJsonPayload(body, true);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const newline = buffer.indexOf(0x0a);
|
|
378
|
+
if (newline === -1) {
|
|
379
|
+
if (!flush) return;
|
|
380
|
+
const line = buffer.toString('utf8');
|
|
381
|
+
buffer = Buffer.alloc(0);
|
|
382
|
+
handleJsonPayload(line, false);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const line = buffer.subarray(0, newline).toString('utf8');
|
|
386
|
+
buffer = buffer.subarray(newline + 1);
|
|
387
|
+
handleJsonPayload(line, false);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
process.stdin.on('data', (chunk) => {
|
|
392
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
393
|
+
drain(false);
|
|
327
394
|
});
|
|
328
395
|
// Parent CLI closed stdin → exit so MCP subprocess does not linger and
|
|
329
396
|
// continue accepting writes after its intended session is over.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const resp = buildResponse(msg);
|
|
335
|
-
if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
|
|
336
|
-
}
|
|
337
|
-
messageQueue.length = 0;
|
|
397
|
+
process.stdin.on('end', () => {
|
|
398
|
+
drain(true);
|
|
399
|
+
process.exit(0);
|
|
400
|
+
});
|
|
338
401
|
}
|
|
339
402
|
|
|
340
403
|
// ── HTTP/SSE mode ─────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ const LOCAL_PLUGIN_CACHE_VERSION = "local";
|
|
|
19
19
|
const SKILL_MARKER = ".xmux-managed-skill";
|
|
20
20
|
const DEFAULT_MCP_PACKAGE = "xmux-bridge";
|
|
21
21
|
const DEFAULT_MCP_BIN = "xmux-lead-mcp";
|
|
22
|
+
const DEFAULT_MCP_NPX_PREFIX = path.join(".cache", "xmux", "npm-prefix");
|
|
22
23
|
|
|
23
24
|
function expandUser(value) {
|
|
24
25
|
const text = String(value || "");
|
|
@@ -172,21 +173,33 @@ function node_mcp_config(serverPath) {
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
function
|
|
176
|
+
function default_mcp_npx_prefix(configPath = "") {
|
|
177
|
+
const fallback = path.join(os.homedir(), DEFAULT_MCP_NPX_PREFIX);
|
|
178
|
+
if (!configPath) return fallback;
|
|
179
|
+
const configDir = path.dirname(abs(configPath));
|
|
180
|
+
if (path.basename(configDir) === ".codex") {
|
|
181
|
+
return path.join(path.dirname(configDir), DEFAULT_MCP_NPX_PREFIX);
|
|
182
|
+
}
|
|
183
|
+
return path.join(configDir, DEFAULT_MCP_NPX_PREFIX);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function npx_mcp_config(packageSpec, binName = DEFAULT_MCP_BIN, npxPrefix = "") {
|
|
187
|
+
const prefix = abs(npxPrefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix());
|
|
176
188
|
return {
|
|
177
189
|
mode: "npx",
|
|
178
190
|
command: "npx",
|
|
179
|
-
args: ["-y", "-p", packageSpec, binName],
|
|
191
|
+
args: ["--prefix", prefix, "-y", "-p", packageSpec, binName],
|
|
180
192
|
package_spec: packageSpec,
|
|
181
193
|
bin: binName,
|
|
182
|
-
|
|
194
|
+
npx_prefix: prefix,
|
|
195
|
+
label: `npx --prefix ${prefix} -y -p ${packageSpec} ${binName}`,
|
|
183
196
|
};
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
function resolve_mcp_config(xmuxInstallDir, opts = {}) {
|
|
187
200
|
if (opts.server_path) return node_mcp_config(opts.server_path);
|
|
188
201
|
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);
|
|
202
|
+
return npx_mcp_config(packageSpec, opts.mcp_bin || DEFAULT_MCP_BIN, opts.mcp_npx_prefix || "");
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts = {}) {
|
|
@@ -198,6 +211,7 @@ function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts =
|
|
|
198
211
|
server_path: mcpConfigOrServerPath.server_path || "",
|
|
199
212
|
package_spec: mcpConfigOrServerPath.package_spec || "",
|
|
200
213
|
bin: mcpConfigOrServerPath.bin || "",
|
|
214
|
+
npx_prefix: mcpConfigOrServerPath.npx_prefix || "",
|
|
201
215
|
label: mcpConfigOrServerPath.label || [
|
|
202
216
|
mcpConfigOrServerPath.command,
|
|
203
217
|
...(mcpConfigOrServerPath.args || []),
|
|
@@ -212,6 +226,12 @@ function mcp_args_toml(mcpConfig) {
|
|
|
212
226
|
return `args = [${mcpConfig.args.map(toml_quote).join(", ")}]`;
|
|
213
227
|
}
|
|
214
228
|
|
|
229
|
+
function ensure_mcp_runtime_dirs(mcpConfig) {
|
|
230
|
+
if (mcpConfig.mode === "npx" && mcpConfig.npx_prefix) {
|
|
231
|
+
fs.mkdirSync(mcpConfig.npx_prefix, { recursive: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
215
235
|
function build_block(mcpConfigOrServerPath, xmuxInstallDir) {
|
|
216
236
|
const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
|
|
217
237
|
const pathEnv = resolve_path_with_node();
|
|
@@ -665,6 +685,7 @@ function parse_args(argv) {
|
|
|
665
685
|
mcp_package: "",
|
|
666
686
|
mcp_version: "",
|
|
667
687
|
mcp_bin: DEFAULT_MCP_BIN,
|
|
688
|
+
mcp_npx_prefix: "",
|
|
668
689
|
};
|
|
669
690
|
for (let i = 0; i < argv.length;) {
|
|
670
691
|
const arg = argv[i];
|
|
@@ -687,6 +708,7 @@ function parse_args(argv) {
|
|
|
687
708
|
"--mcp-package",
|
|
688
709
|
"--mcp-version",
|
|
689
710
|
"--mcp-bin",
|
|
711
|
+
"--mcp-npx-prefix",
|
|
690
712
|
].includes(arg) && i + 1 < argv.length) {
|
|
691
713
|
const key = arg.slice(2).replace(/-/g, "_");
|
|
692
714
|
opts[key] = expandUser(argv[i + 1]);
|
|
@@ -725,6 +747,7 @@ function resolve_config_path(opts) {
|
|
|
725
747
|
function main(argv = process.argv.slice(2)) {
|
|
726
748
|
const opts = parse_args(argv);
|
|
727
749
|
const configPath = resolve_config_path(opts);
|
|
750
|
+
opts.mcp_npx_prefix = abs(opts.mcp_npx_prefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix(configPath));
|
|
728
751
|
const scriptInstallDir = path.dirname(path.dirname(abs(__filename)));
|
|
729
752
|
const rawInstallDir = abs(opts.xmux_install_dir || scriptInstallDir);
|
|
730
753
|
const xmuxInstallDir = stable_homebrew_xmux_install_dir(rawInstallDir);
|
|
@@ -736,6 +759,8 @@ function main(argv = process.argv.slice(2)) {
|
|
|
736
759
|
return doctor_codex(configPath, xmuxInstallDir, mcpConfig, opts.skills_dir, opts.quiet);
|
|
737
760
|
}
|
|
738
761
|
|
|
762
|
+
ensure_mcp_runtime_dirs(mcpConfig);
|
|
763
|
+
|
|
739
764
|
let content = remove_xmux_blocks(read_text(configPath));
|
|
740
765
|
if (opts.remove) {
|
|
741
766
|
content = remove_codex_shell_environment(content, xmuxInstallDir);
|
|
@@ -788,6 +813,7 @@ module.exports = {
|
|
|
788
813
|
remove_xmux_blocks,
|
|
789
814
|
build_block,
|
|
790
815
|
default_mcp_package_spec,
|
|
816
|
+
default_mcp_npx_prefix,
|
|
791
817
|
resolve_mcp_config,
|
|
792
818
|
path_with_xmux_bin,
|
|
793
819
|
ensure_codex_shell_environment,
|
package/xmux-lead-mcp-server.js
CHANGED
|
@@ -451,36 +451,94 @@ function buildResponse(msg) {
|
|
|
451
451
|
return null;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
function writeMcpResponse(resp, framed) {
|
|
455
|
+
const body = JSON.stringify(resp);
|
|
456
|
+
if (framed) {
|
|
457
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
|
|
458
|
+
} else {
|
|
459
|
+
process.stdout.write(body + '\n');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function handleStdioMessage(msg, framed) {
|
|
464
|
+
const resp = buildResponse(msg);
|
|
465
|
+
if (resp) writeMcpResponse(resp, framed);
|
|
466
|
+
}
|
|
467
|
+
|
|
454
468
|
function startStdio() {
|
|
455
|
-
|
|
456
|
-
const messageQueue = [];
|
|
457
|
-
let ready = false;
|
|
469
|
+
let buffer = Buffer.alloc(0);
|
|
458
470
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
471
|
+
function parseContentLengthHeader(header) {
|
|
472
|
+
for (const line of header.split(/\r?\n/)) {
|
|
473
|
+
const match = line.match(/^Content-Length:\s*(\d+)\s*$/i);
|
|
474
|
+
if (match) return Number.parseInt(match[1], 10);
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function handleJsonPayload(payload, framed) {
|
|
480
|
+
if (!payload.trim()) return;
|
|
462
481
|
let msg;
|
|
463
482
|
try {
|
|
464
|
-
msg = JSON.parse(
|
|
483
|
+
msg = JSON.parse(payload);
|
|
465
484
|
} catch (_) {
|
|
466
485
|
return;
|
|
467
486
|
}
|
|
487
|
+
handleStdioMessage(msg, framed);
|
|
488
|
+
}
|
|
468
489
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
490
|
+
function drain(flush = false) {
|
|
491
|
+
while (buffer.length) {
|
|
492
|
+
while (buffer[0] === 0x0a || buffer[0] === 0x0d) buffer = buffer.subarray(1);
|
|
493
|
+
if (!buffer.length) return;
|
|
494
|
+
|
|
495
|
+
const text = buffer.toString('utf8');
|
|
496
|
+
if (text.toLowerCase().startsWith('content-length:')) {
|
|
497
|
+
let headerEnd = text.indexOf('\r\n\r\n');
|
|
498
|
+
let separatorLength = 4;
|
|
499
|
+
if (headerEnd === -1) {
|
|
500
|
+
headerEnd = text.indexOf('\n\n');
|
|
501
|
+
separatorLength = 2;
|
|
502
|
+
}
|
|
503
|
+
if (headerEnd === -1) return;
|
|
504
|
+
|
|
505
|
+
const header = text.slice(0, headerEnd);
|
|
506
|
+
const contentLength = parseContentLengthHeader(header);
|
|
507
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
508
|
+
buffer = Buffer.alloc(0);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + separatorLength), 'utf8');
|
|
513
|
+
if (buffer.length < bodyStart + contentLength) return;
|
|
514
|
+
const body = buffer.subarray(bodyStart, bodyStart + contentLength).toString('utf8');
|
|
515
|
+
buffer = buffer.subarray(bodyStart + contentLength);
|
|
516
|
+
handleJsonPayload(body, true);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const newline = buffer.indexOf(0x0a);
|
|
521
|
+
if (newline === -1) {
|
|
522
|
+
if (!flush) return;
|
|
523
|
+
const line = buffer.toString('utf8');
|
|
524
|
+
buffer = Buffer.alloc(0);
|
|
525
|
+
handleJsonPayload(line, false);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const line = buffer.subarray(0, newline).toString('utf8');
|
|
529
|
+
buffer = buffer.subarray(newline + 1);
|
|
530
|
+
handleJsonPayload(line, false);
|
|
474
531
|
}
|
|
475
|
-
});
|
|
476
|
-
rl.on('close', () => process.exit(0));
|
|
477
|
-
|
|
478
|
-
ready = true;
|
|
479
|
-
for (const msg of messageQueue) {
|
|
480
|
-
const resp = buildResponse(msg);
|
|
481
|
-
if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
|
|
482
532
|
}
|
|
483
|
-
|
|
533
|
+
|
|
534
|
+
process.stdin.on('data', (chunk) => {
|
|
535
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
536
|
+
drain(false);
|
|
537
|
+
});
|
|
538
|
+
process.stdin.on('end', () => {
|
|
539
|
+
drain(true);
|
|
540
|
+
process.exit(0);
|
|
541
|
+
});
|
|
484
542
|
}
|
|
485
543
|
|
|
486
544
|
startStdio();
|
package/xmux.zsh
CHANGED
|
@@ -71,7 +71,7 @@ _xmux_refresh_home() {
|
|
|
71
71
|
|
|
72
72
|
_xmux_refresh_paths
|
|
73
73
|
|
|
74
|
-
XMUX_VERSION="1.0.
|
|
74
|
+
XMUX_VERSION="1.0.40"
|
|
75
75
|
XMUX_LEAD_AGENT="${XMUX_LEAD_AGENT:-codex-lead}"
|
|
76
76
|
|
|
77
77
|
_xmux_q() {
|
|
@@ -4208,8 +4208,8 @@ _xmux_run_codex_setup_script() {
|
|
|
4208
4208
|
|
|
4209
4209
|
_xmux_setup_codex_usage() {
|
|
4210
4210
|
cat >&2 <<'EOF'
|
|
4211
|
-
Usage: xmux setup-codex [--skills-dir <dir>] [--without-skills] [--mcp-package <package[@version]>] [--mcp-version <version>]
|
|
4212
|
-
xmux doctor-codex [--mcp-package <package[@version]>] [--mcp-version <version>]
|
|
4211
|
+
Usage: xmux setup-codex [--skills-dir <dir>] [--without-skills] [--mcp-package <package[@version]>] [--mcp-version <version>] [--mcp-npx-prefix <dir>]
|
|
4212
|
+
xmux doctor-codex [--mcp-package <package[@version]>] [--mcp-version <version>] [--mcp-npx-prefix <dir>]
|
|
4213
4213
|
xmux remove-codex
|
|
4214
4214
|
EOF
|
|
4215
4215
|
}
|
|
@@ -4224,7 +4224,7 @@ _xmux_cmd_setup_codex() {
|
|
|
4224
4224
|
setup_args+=("$arg")
|
|
4225
4225
|
shift
|
|
4226
4226
|
;;
|
|
4227
|
-
--home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin)
|
|
4227
|
+
--home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin|--mcp-npx-prefix)
|
|
4228
4228
|
[[ $# -ge 2 ]] || { echo "error: $arg requires a value." >&2; return 1; }
|
|
4229
4229
|
setup_args+=("$arg" "$2")
|
|
4230
4230
|
shift 2
|
|
@@ -4253,7 +4253,7 @@ _xmux_cmd_doctor_codex() {
|
|
|
4253
4253
|
doctor_args+=("$arg")
|
|
4254
4254
|
shift
|
|
4255
4255
|
;;
|
|
4256
|
-
--home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin)
|
|
4256
|
+
--home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin|--mcp-npx-prefix)
|
|
4257
4257
|
[[ $# -ge 2 ]] || { echo "error: $arg requires a value." >&2; return 1; }
|
|
4258
4258
|
doctor_args+=("$arg" "$2")
|
|
4259
4259
|
shift 2
|