Skip to content

Commit e2251f3

Browse files
authored
Merge pull request #1846 from DeusData/codex/issue-1753-clients-forwarding
fix(install): forward clients selector
2 parents 5743563 + 902f85b commit e2251f3

2 files changed

Lines changed: 208 additions & 15 deletions

File tree

install.sh

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ main() {
1919
REPO="DeusData/codebase-memory-mcp"
2020
INSTALL_DIR="$HOME/.local/bin"
2121
SKIP_CONFIG=false
22+
CLIENTS_SET=false
23+
CLIENTS=""
2224
CBM_DOWNLOAD_URL="${CBM_DOWNLOAD_URL:-https://github.com/${REPO}/releases/latest/download}"
2325

2426
# Security: every remote hop must remain HTTPS. Plain HTTP is accepted only
@@ -79,26 +81,46 @@ download_file() {
7981
fi
8082
}
8183

82-
for arg in "$@"; do
83-
case "$arg" in
84-
--dir=*) INSTALL_DIR="${arg#--dir=}" ;;
85-
--skip-config) SKIP_CONFIG=true ;;
84+
while [ "$#" -gt 0 ]; do
85+
case "$1" in
86+
--dir=*)
87+
INSTALL_DIR="${1#--dir=}"
88+
shift
89+
;;
90+
--dir)
91+
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
92+
echo "install.sh: '--dir' needs a value. Please consult --help." >&2
93+
exit 2
94+
fi
95+
INSTALL_DIR="$2"
96+
shift 2
97+
;;
98+
--clients=*)
99+
CLIENTS_SET=true
100+
CLIENTS="${1#--clients=}"
101+
shift
102+
;;
103+
--skip-config)
104+
SKIP_CONFIG=true
105+
shift
106+
;;
86107
--help|-h)
87-
echo "Usage: install.sh [--dir=<path>] [--skip-config]"
88-
echo " --dir PATH Install directory (default: ~/.local/bin)"
89-
echo " --skip-config Skip automatic agent configuration"
108+
echo "Usage: install.sh [--dir=<path>] [--clients=<list>] [--skip-config]"
109+
echo " --dir PATH Install directory (default: ~/.local/bin)"
110+
echo " --clients LIST Configure only comma-separated clients"
111+
echo " --skip-config Skip automatic agent configuration"
90112
exit 0
91113
;;
114+
-*)
115+
echo "install.sh: unknown option '$1'. Please consult --help." >&2
116+
exit 2
117+
;;
118+
*)
119+
echo "install.sh: unexpected argument '$1'. Please consult --help." >&2
120+
exit 2
121+
;;
92122
esac
93123
done
94-
# Handle --dir <path> (space-separated)
95-
prev=""
96-
for arg in "$@"; do
97-
if [ "$prev" = "--dir" ]; then
98-
INSTALL_DIR="$arg"
99-
fi
100-
prev="$arg"
101-
done
102124

103125
detect_os() {
104126
case "$(uname -s)" in
@@ -314,6 +336,9 @@ echo "Verified candidate: $CANDIDATE_VERSION"
314336

315337
DEST="$INSTALL_DIR/codebase-memory-mcp"
316338
INSTALL_ARGS=(-y --force "--dir=$INSTALL_DIR")
339+
if [ "$CLIENTS_SET" = true ]; then
340+
INSTALL_ARGS+=("--clients=$CLIENTS")
341+
fi
317342
if [ "$SKIP_CONFIG" = true ]; then
318343
INSTALL_ARGS+=(--skip-config)
319344
fi

tests/test_smoke_fixture_contract.sh

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
88
python3 - "$ROOT" <<'PY'
99
from __future__ import annotations
1010
11+
import hashlib
12+
import os
1113
import pathlib
1214
import re
1315
import subprocess
1416
import sys
17+
import tarfile
1518
import tempfile
1619
import time
1720
import urllib.request
@@ -415,6 +418,171 @@ require(
415418
"scripts/test.sh must run the smoke fixture contract",
416419
)
417420
421+
# Functional wrapper contract: install.sh must pass its wrapper-only selectors
422+
# to the downloaded candidate, while retaining the existing dir/skip controls.
423+
# Native Windows ships and exercises install.ps1; the Unix wrapper is covered
424+
# on both macOS and Linux venue legs.
425+
if sys.platform != "win32":
426+
with tempfile.TemporaryDirectory(prefix="cbm-install-wrapper-") as temp:
427+
temp_path = pathlib.Path(temp)
428+
fixture = temp_path / "fixture"
429+
payload = temp_path / "payload"
430+
fixture.mkdir()
431+
payload.mkdir()
432+
433+
uname_s = subprocess.check_output(["uname", "-s"], text=True).strip()
434+
uname_m = subprocess.check_output(["uname", "-m"], text=True).strip()
435+
os_name = "darwin" if uname_s == "Darwin" else "linux"
436+
if uname_m in ("arm64", "aarch64"):
437+
arch_name = "arm64"
438+
else:
439+
arch_name = "amd64"
440+
portable = "-portable" if os_name == "linux" else ""
441+
archive_name = (
442+
f"codebase-memory-mcp-{os_name}-{arch_name}{portable}.tar.gz"
443+
)
444+
archive = fixture / archive_name
445+
446+
candidate = payload / "codebase-memory-mcp"
447+
candidate.write_text(
448+
"#!/usr/bin/env bash\n"
449+
"set -euo pipefail\n"
450+
"if [ \"${1:-}\" = --version ]; then echo 'cbm fixture 0'; exit 0; fi\n"
451+
"if [ \"${1:-}\" != install ]; then exit 64; fi\n"
452+
"printf '%s\\n' \"$@\" > \"$CBM_INSTALL_ARG_LOG\"\n"
453+
"target=''\n"
454+
"for arg in \"$@\"; do case \"$arg\" in --dir=*) target=${arg#--dir=} ;; esac; done\n"
455+
"[ -n \"$target\" ] || exit 65\n"
456+
"mkdir -p \"$target\"\n"
457+
"cp \"$0\" \"$target/codebase-memory-mcp\"\n",
458+
encoding="utf-8",
459+
)
460+
candidate.chmod(0o755)
461+
(payload / "LICENSE").write_text("fixture license\n", encoding="utf-8")
462+
(payload / "install.sh").write_text(install_script, encoding="utf-8")
463+
(payload / "THIRD_PARTY_NOTICES.md").write_text(
464+
"fixture notices\n", encoding="utf-8"
465+
)
466+
with tarfile.open(archive, "w:gz") as bundle:
467+
for name in (
468+
"codebase-memory-mcp",
469+
"LICENSE",
470+
"install.sh",
471+
"THIRD_PARTY_NOTICES.md",
472+
):
473+
bundle.add(payload / name, arcname=name)
474+
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
475+
(fixture / "checksums.txt").write_text(
476+
f"{digest} {archive_name}\n", encoding="ascii"
477+
)
478+
479+
# Replace curl only inside the subprocess environment. The wrapper still
480+
# performs its real archive/checksum/extraction flow, but all bytes come
481+
# from this generated fixture and no socket is opened.
482+
fake_bin = temp_path / "fake-bin"
483+
fake_bin.mkdir()
484+
fake_curl = fake_bin / "curl"
485+
fake_curl.write_text(
486+
"#!/usr/bin/env python3\n"
487+
"import os, pathlib, shutil, sys, urllib.parse\n"
488+
"args = sys.argv[1:]\n"
489+
"target = pathlib.Path(args[args.index('-o') + 1])\n"
490+
"name = pathlib.PurePosixPath(urllib.parse.urlparse(args[-1]).path).name\n"
491+
"shutil.copyfile(pathlib.Path(os.environ['CBM_INSTALL_FIXTURE']) / name, target)\n",
492+
encoding="utf-8",
493+
)
494+
fake_curl.chmod(0o755)
495+
496+
wrapper = root / "install.sh"
497+
help_result = subprocess.run(
498+
[str(wrapper), "--help"],
499+
text=True,
500+
stdout=subprocess.PIPE,
501+
stderr=subprocess.STDOUT,
502+
timeout=10,
503+
check=False,
504+
)
505+
require(
506+
help_result.returncode == 0
507+
and "--dir" in help_result.stdout
508+
and "--skip-config" in help_result.stdout
509+
and "--clients" in help_result.stdout,
510+
"install.sh --help must document dir, skip-config, and clients",
511+
)
512+
513+
base_env = dict(os.environ)
514+
base_env.update(
515+
CBM_DOWNLOAD_URL="http://127.0.0.1:9",
516+
CBM_INSTALL_FIXTURE=str(fixture),
517+
HOME=str(temp_path / "home"),
518+
PATH=f"{fake_bin}{os.pathsep}{base_env['PATH']}",
519+
)
520+
pathlib.Path(base_env["HOME"]).mkdir()
521+
522+
def run_wrapper(
523+
log_name: str, *arguments: str
524+
) -> tuple[subprocess.CompletedProcess[str], list[str]]:
525+
argument_log = temp_path / log_name
526+
env = dict(base_env)
527+
env["CBM_INSTALL_ARG_LOG"] = str(argument_log)
528+
result = subprocess.run(
529+
[str(wrapper), *arguments],
530+
env=env,
531+
text=True,
532+
stdout=subprocess.PIPE,
533+
stderr=subprocess.STDOUT,
534+
timeout=20,
535+
check=False,
536+
)
537+
logged = (
538+
argument_log.read_text(encoding="utf-8").splitlines()
539+
if argument_log.is_file()
540+
else []
541+
)
542+
return result, logged
543+
544+
unknown, unknown_args = run_wrapper(
545+
"unknown-args.log", "--definitely-not-a-wrapper-flag"
546+
)
547+
require(
548+
unknown.returncode == 2
549+
and "Please consult --help." in unknown.stdout
550+
and not unknown_args,
551+
"install.sh must reject unknown flags before executing the candidate",
552+
)
553+
554+
selected_dir = temp_path / "selected-bin"
555+
selected, selected_args = run_wrapper(
556+
"selected-args.log",
557+
f"--dir={selected_dir}",
558+
"--clients=claude,codex",
559+
"--skip-config",
560+
)
561+
require(
562+
selected.returncode == 0
563+
and selected_args
564+
== [
565+
"install",
566+
"-y",
567+
"--force",
568+
f"--dir={selected_dir}",
569+
"--clients=claude,codex",
570+
"--skip-config",
571+
],
572+
"install.sh must forward the explicit clients selector with dir and skip-config",
573+
)
574+
575+
ordinary_dir = temp_path / "ordinary-bin"
576+
ordinary, ordinary_args = run_wrapper(
577+
"ordinary-args.log", f"--dir={ordinary_dir}"
578+
)
579+
require(
580+
ordinary.returncode == 0
581+
and ordinary_args
582+
== ["install", "-y", "--force", f"--dir={ordinary_dir}"],
583+
"install.sh without selectors must preserve the ordinary install arguments",
584+
)
585+
418586
# Functional check: the helper must publish a live kernel-assigned port and
419587
# serve the exact expected artifact. This is intentionally build-free.
420588
if helper.is_file():

0 commit comments

Comments
 (0)