Merge pull request #42 from 0xrsydn/fix/mcp-npx-hermes

fix(npm): install the binary on first run when postinstall was skipped
This commit is contained in:
brutaljokerz 2026-10-16 11:46:37 +07:00 • committed by GitHub
commit 53fa283805
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 165 additions and 45 deletions

View file

@ -23,9 +23,8 @@ npm install --global idx-cli
``` ```
The npm package downloads the same checksum-verified release binary during The npm package downloads the same checksum-verified release binary during
`postinstall`. pnpm 10+ and bun skip install scripts by default; allow them `postinstall`, or on first run when the package manager skips install scripts
with `pnpm add --global --allow-build=idx-cli idx-cli` or (npm 12 under `npx`, pnpm 10+, bun). See
`bun add --global --trust idx-cli`. See
[docs/NPM_DISTRIBUTION.md](docs/NPM_DISTRIBUTION.md). [docs/NPM_DISTRIBUTION.md](docs/NPM_DISTRIBUTION.md).
Prebuilt binaries cover Linux x64/arm64 (static musl) and macOS arm64/x64. Prebuilt binaries cover Linux x64/arm64 (static musl) and macOS arm64/x64.

View file

@ -43,11 +43,16 @@ version `0.3.1` downloads assets from the `v0.3.1` GitHub Release.
The postinstall script downloads `SHA256SUMS` from the same release and The postinstall script downloads `SHA256SUMS` from the same release and
refuses to install a binary whose checksum does not match. refuses to install a binary whose checksum does not match.
### pnpm and bun ### When postinstall does not run
pnpm 10+ and bun do not run dependency lifecycle scripts by default, so the npm 12 blocks install scripts of unapproved packages under `npx` and
binary is never downloaded and `idx` reports that it is missing. Allow the `npm install -g` (its `allow-scripts` policy), and pnpm 10+ and bun skip
postinstall script explicitly: dependency lifecycle scripts by default. The launcher then runs the installer
itself on first start, logging to stderr so the `idx-mcp` stdout channel stays
clean; it fails only when that download fails too. Hermes Agent runs `npx` with
its own npm 12, which is how 0.3.1 broke there (issue #41).
To install the binary up front instead, allow the script explicitly:
```bash ```bash
# pnpm # pnpm
@ -55,15 +60,17 @@ pnpm add --global --allow-build=idx-cli idx-cli
# bun # bun
bun add --global --trust idx-cli bun add --global --trust idx-cli
```
Plain `npm install` and `npx` run the postinstall script normally. # npm 12
npm install --global --allow-scripts=idx-cli idx-cli
```
## Local smoke test ## Local smoke test
The smoke script builds or reuses `target/release/idx`, packs the npm wrapper, The smoke script builds or reuses `target/release/idx`, packs the npm wrapper,
installs the tarball into a new temporary directory, and runs the installed installs the tarball into a new temporary directory, and runs the installed
`idx` command. It uses `IDX_BINARY_URL` with a local `file:` URL, so the test `idx` command, then installs `idx-mcp` again with `--ignore-scripts` to check
the launcher's first-run install. It uses `IDX_BINARY_URL` with a local `file:` URL, so the test
does not need a registry publish or a network download. does not need a registry publish or a network download.
```bash ```bash

View file

@ -86,8 +86,19 @@ and agent behavior varies between runs, so it is never part of the gate.
nix develop .#agents --command tests/agents/agent-smoke.sh # every agent found nix develop .#agents --command tests/agents/agent-smoke.sh # every agent found
nix develop .#agents --command tests/agents/agent-smoke.sh --agents claude,codex --runs 3 nix develop .#agents --command tests/agents/agent-smoke.sh --agents claude,codex --runs 3
tests/agents/agent-smoke.sh --live --case shareholders --keep # real data tests/agents/agent-smoke.sh --live --case shareholders --keep # real data
tests/agents/agent-smoke.sh --npx idx-mcp # the published npm package
tests/agents/agent-smoke.sh --npx "$(cd npm/idx-mcp && realpath "$(npm pack --silent)")" # this checkout's wrapper
``` ```
`--npx <spec>` registers `npx -y <spec>` the way users configure it, so each
agent's own `npx` handling and a cold npm install are part of the test. It
runs the release binary, so it implies `--live`; `IDX_BINARY_URL` swaps in a
local one. Hermes Agent replaces a bare `npx` with its own generic-Linux
Node, which NixOS cannot load; on NixOS use `nix develop .#agents-fhs`, which
runs every agent in an FHS sandbox (and `agent-fhs hermes` for an interactive
session). Hermes logs a server's stderr to `$HERMES_HOME/logs/mcp-stderr.log`;
each run keeps a copy next to its output as `<case>.<run>.mcp-stderr`.
- Each agent gets an isolated config that registers only the local server; - Each agent gets an isolated config that registers only the local server;
your own agent settings and MCP servers are not loaded or changed. Logins your own agent settings and MCP servers are not loaded or changed. Logins
are copied from the agent's home (Codex `auth.json`, pi and Hermes auth are copied from the agent's home (Codex `auth.json`, pi and Hermes auth
@ -95,7 +106,8 @@ tests/agents/agent-smoke.sh --live --case shareholders --keep # real da
output but still deletes every copied agent config. output but still deletes every copied agent config.
- Tools are auto-approved for the run (Claude `dontAsk` with - Tools are auto-approved for the run (Claude `dontAsk` with
`mcp__idx__*`, Codex `default_tools_approval_mode="approve"`); Claude and pi `mcp__idx__*`, Codex `default_tools_approval_mode="approve"`); Claude and pi
also get no built-in tools, so they can only answer through MCP. also get no built-in tools, and Hermes only the `idx` toolset, so they
can only answer through MCP.
- `IDX_AGENT_MODEL_<AGENT>` picks a model (for example - `IDX_AGENT_MODEL_<AGENT>` picks a model (for example
`IDX_AGENT_MODEL_CLAUDE=sonnet`). `IDX_AGENT_EXEC_PREFIX` prefixes every `IDX_AGENT_MODEL_CLAUDE=sonnet`). `IDX_AGENT_EXEC_PREFIX` prefixes every
agent command, for hosts that inject API keys through a wrapper. agent command, for hosts that inject API keys through a wrapper.

View file

@ -160,8 +160,27 @@
./scripts/build-ownership-snapshot.sh ./scripts/build-ownership-snapshot.sh
]; ];
}; };
# `nix develop .#agents-fhs` (Linux): the agents shell, with every agent
# run inside an FHS sandbox. Hermes runs a bare `npx` with its own
# generic-Linux Node, which NixOS cannot load, so testing the
# `npx -y idx-mcp` setup users have needs this on NixOS.
# `agent-fhs <cmd>` runs any command (e.g. an interactive `hermes`) inside it.
agentsFhsShell =
let
agentFhs = pkgs.buildFHSEnv {
name = "agent-fhs";
targetPkgs = p: [ p.stdenv.cc.cc.lib p.zlib p.openssl ];
runScript = pkgs.writeShellScript "agent-fhs-run" ''exec "''${@:-bash}"'';
};
in in
{ self.devShells.${system}.agents.overrideAttrs (old: {
shellHook = (old.shellHook or "") + ''
export PATH="${agentFhs}/bin:$PATH"
export IDX_AGENT_EXEC_PREFIX="''${IDX_AGENT_EXEC_PREFIX:+$IDX_AGENT_EXEC_PREFIX }agent-fhs"
'';
});
in
lib.recursiveUpdate {
packages.default = idxPackage; packages.default = idxPackage;
packages.idx-mcp = mcpPackage; packages.idx-mcp = mcpPackage;
packages.ownership-publisher = ownershipPublisher; packages.ownership-publisher = ownershipPublisher;
@ -352,6 +371,6 @@
RUST_BACKTRACE = "1"; RUST_BACKTRACE = "1";
}; };
}; };
} } (lib.optionalAttrs pkgs.stdenv.isLinux { devShells.agents-fhs = agentsFhsShell; })
); );
} }

View file

@ -3,7 +3,7 @@
const fs = require("node:fs"); const fs = require("node:fs");
const path = require("node:path"); const path = require("node:path");
const { spawn } = require("node:child_process"); const { spawn, spawnSync } = require("node:child_process");
const packageManifest = JSON.parse( const packageManifest = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"), fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
@ -13,6 +13,15 @@ const packageName = packageManifest.name;
const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : nativeBinary; const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : nativeBinary;
const binaryPath = path.join(__dirname, "..", "bin", binaryName); const binaryPath = path.join(__dirname, "..", "bin", binaryName);
// postinstall never ran (ignore-scripts) or its download failed: install now.
// Its log goes to stderr, since stdout is the MCP channel for idx-mcp.
if (!fs.existsSync(binaryPath)) {
console.error(`${packageName}: binary not installed; running the installer`);
spawnSync(process.execPath, [path.join(__dirname, "install.js")], {
stdio: ["ignore", 2, 2],
});
}
if (!fs.existsSync(binaryPath)) { if (!fs.existsSync(binaryPath)) {
console.error(`${packageName}: installed binary not found at ${binaryPath}`); console.error(`${packageName}: installed binary not found at ${binaryPath}`);
console.error(`${packageName}: rerun npm install or set IDX_BINARY_URL for a local binary`); console.error(`${packageName}: rerun npm install or set IDX_BINARY_URL for a local binary`);

View file

@ -80,4 +80,19 @@ mkdir -p "$npm_cache"
smoke_package "$repo_root" idx "Usage: idx" smoke_package "$repo_root" idx "Usage: idx"
"${smoke_dir}/idx/install/node_modules/.bin/idx" config --help >/dev/null "${smoke_dir}/idx/install/node_modules/.bin/idx" config --help >/dev/null
smoke_package "${repo_root}/npm/idx-mcp" idx-mcp "Usage: idx-mcp" smoke_package "${repo_root}/npm/idx-mcp" idx-mcp "Usage: idx-mcp"
echo "npm smoke: PASS (idx-cli and idx-mcp: pack, install, --help)"
# Without postinstall (ignore-scripts, or a failed download) the wrapper
# installs the binary on first run, logging only to stderr: stdout is the MCP
# channel, and a client sees an early exit as "Connection closed".
echo "npm smoke: installing idx-mcp with --ignore-scripts"
no_scripts_dir="${smoke_dir}/idx-mcp-no-scripts"
npm_config_cache="$npm_cache" npm install --prefix "$no_scripts_dir" "${smoke_dir}"/idx-mcp/idx-mcp-*.tgz \
--no-save --no-package-lock --no-audit --no-fund --ignore-scripts
IDX_BINARY_URL="${repo_root}/target/release/idx-mcp" \
"${no_scripts_dir}/node_modules/.bin/idx-mcp" --help > "${smoke_dir}/no-scripts-help.txt"
if ! head -n 1 "${smoke_dir}/no-scripts-help.txt" | grep -q "MCP server"; then
echo "npm smoke: idx-mcp without postinstall did not start cleanly:" >&2
cat "${smoke_dir}/no-scripts-help.txt" >&2
exit 1
fi
echo "npm smoke: PASS (idx-cli and idx-mcp: pack, install, --help; idx-mcp without postinstall)"

View file

@ -20,6 +20,9 @@ Options:
--runs <n> Runs per agent and case (default: 1) --runs <n> Runs per agent and case (default: 1)
--live Real upstream data and the published ownership snapshot --live Real upstream data and the published ownership snapshot
instead of mock fixtures; answer text is not checked instead of mock fixtures; answer text is not checked
--npx <spec> Register `npx -y <spec>` as users do, instead of the local
build: `idx-mcp` (published) or an `npm pack` tarball.
Runs the release binary, so it implies --live
--keep Keep the work directory (raw agent output, results.jsonl) --keep Keep the work directory (raw agent output, results.jsonl)
--help Show this help --help Show this help
@ -29,6 +32,8 @@ Environment:
IDX_AGENT_MODEL_<AGENT> Model override, e.g. IDX_AGENT_MODEL_CLAUDE=sonnet, IDX_AGENT_MODEL_<AGENT> Model override, e.g. IDX_AGENT_MODEL_CLAUDE=sonnet,
IDX_AGENT_MODEL_OPENCODE=opencode-go/glm-5.3 IDX_AGENT_MODEL_OPENCODE=opencode-go/glm-5.3
IDX_AGENT_TIMEOUT Seconds per agent run (default: 300) IDX_AGENT_TIMEOUT Seconds per agent run (default: 300)
IDX_BINARY_URL With --npx: binary the npm package installs instead
of the release download (see docs/NPM_DISTRIBUTION.md)
Exit status: 0 when every agent passed every case at least once, 1 when some Exit status: 0 when every agent passed every case at least once, 1 when some
agent failed a case in all runs, 2 on usage or setup errors. agent failed a case in all runs, 2 on usage or setup errors.
@ -43,6 +48,7 @@ only_cases=()
runs=1 runs=1
live=0 live=0
keep=0 keep=0
npx_spec=
timeout_secs="${IDX_AGENT_TIMEOUT:-300}" timeout_secs="${IDX_AGENT_TIMEOUT:-300}"
read -r -a exec_prefix <<< "${IDX_AGENT_EXEC_PREFIX:-}" read -r -a exec_prefix <<< "${IDX_AGENT_EXEC_PREFIX:-}"
@ -52,6 +58,7 @@ while [[ $# -gt 0 ]]; do
--case) only_cases+=("${2:?}"); shift 2 ;; --case) only_cases+=("${2:?}"); shift 2 ;;
--runs) runs="${2:?}"; shift 2 ;; --runs) runs="${2:?}"; shift 2 ;;
--live) live=1; shift ;; --live) live=1; shift ;;
--npx) npx_spec="${2:?}"; live=1; shift 2 ;;
--keep) keep=1; shift ;; --keep) keep=1; shift ;;
--help|-h) usage; exit 0 ;; --help|-h) usage; exit 0 ;;
*) usage >&2; exit 2 ;; *) usage >&2; exit 2 ;;
@ -63,7 +70,7 @@ if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4)));
echo "agent-smoke: bash 4.4+ is required (run it inside nix develop)" >&2 echo "agent-smoke: bash 4.4+ is required (run it inside nix develop)" >&2
exit 2 exit 2
fi fi
for tool in jq cargo timeout; do for tool in jq cargo timeout ${npx_spec:+npx}; do
command -v "$tool" >/dev/null || { echo "agent-smoke: $tool is required" >&2; exit 2; } command -v "$tool" >/dev/null || { echo "agent-smoke: $tool is required" >&2; exit 2; }
done done
@ -118,16 +125,36 @@ else
done done
fi fi
# One launcher for every agent: isolated data dirs, and the idx-core crate # One server definition for every agent, as {command, args, env}.
# as working directory (the mock provider reads tests/fixtures from there). if [[ -n "$npx_spec" ]]; then
launcher="$work/idx-mcp" # What users configure, so each agent's own npx handling is exercised
{ # (Hermes swaps in its managed Node). A fresh npm cache makes the first
# agent run a cold install; agents may filter env, so pass it explicitly.
export npm_config_cache="$work/npm-cache"
server_env=("${data_env[@]}" "npm_config_cache=$npm_config_cache")
[[ -z "${IDX_BINARY_URL:-}" ]] || server_env+=("IDX_BINARY_URL=$IDX_BINARY_URL")
# npx runs a tarball only as a package to install: `npx -y -p file:<tgz> idx-mcp`.
if [[ "$npx_spec" == *.tgz && ! -f "$npx_spec" ]]; then
echo "agent-smoke: tarball not found: $npx_spec" >&2; exit 2
fi
npx_args=(-y "$npx_spec")
[[ ! -f "$npx_spec" ]] || npx_args=(-y -p "file:$(realpath "$npx_spec")" idx-mcp)
server_json="$(jq -nc --argjson args "$(printf '%s\n' "${npx_args[@]}" | jq -Rnc '[inputs]')" '{command: "npx", args: $args,
env: ($ARGS.positional | map(capture("^(?<key>[^=]+)=(?<value>.*)$")) | from_entries)}' \
--args "${server_env[@]}")"
else
# A launcher with isolated data dirs, and the idx-core crate as working
# directory (the mock provider reads tests/fixtures from there).
launcher="$work/idx-mcp"
{
echo '#!/usr/bin/env bash' echo '#!/usr/bin/env bash'
printf 'export %q\n' "${data_env[@]}" printf 'export %q\n' "${data_env[@]}"
printf 'cd %q\n' "$repo_root/crates/idx-core" printf 'cd %q\n' "$repo_root/crates/idx-core"
printf 'exec %q "$@"\n' "$repo_root/target/debug/idx-mcp" printf 'exec %q "$@"\n' "$repo_root/target/debug/idx-mcp"
} > "$launcher" } > "$launcher"
chmod +x "$launcher" chmod +x "$launcher"
server_json="$(jq -nc --arg cmd "$launcher" '{command: $cmd, args: [], env: {}}')"
fi
# --- per-agent setup, run, and tool-call extraction --------------------------- # --- per-agent setup, run, and tool-call extraction ---------------------------
# Each run_<agent> writes raw output to $out.raw, the answer to $out.answer, and # Each run_<agent> writes raw output to $out.raw, the answer to $out.answer, and
@ -137,7 +164,7 @@ chmod +x "$launcher"
copy_if_present() { [[ -f "$1" ]] && cp "$1" "$2/"; return 0; } copy_if_present() { [[ -f "$1" ]] && cp "$1" "$2/"; return 0; }
setup_claude() { setup_claude() {
printf '{"mcpServers":{"idx":{"type":"stdio","command":"%s","args":[]}}}\n' "$launcher" > "$work/claude-mcp.json" jq -n --argjson s "$server_json" '{mcpServers: {idx: ({type: "stdio"} + $s)}}' > "$work/claude-mcp.json"
} }
run_claude() { run_claude() {
local prompt="$1" out="$2" model=() local prompt="$1" out="$2" model=()
@ -158,11 +185,15 @@ setup_codex() {
copy_if_present "${CODEX_HOME:-$HOME/.codex}/auth.json" "$work/codex" copy_if_present "${CODEX_HOME:-$HOME/.codex}/auth.json" "$work/codex"
} }
run_codex() { run_codex() {
local prompt="$1" out="$2" model=() local prompt="$1" out="$2" model=() server=()
[[ -n "${IDX_AGENT_MODEL_CODEX:-}" ]] && model=(-m "$IDX_AGENT_MODEL_CODEX") [[ -n "${IDX_AGENT_MODEL_CODEX:-}" ]] && model=(-m "$IDX_AGENT_MODEL_CODEX")
# -c values are TOML; JSON strings and arrays of them are valid TOML.
mapfile -t server < <(jq -r '"mcp_servers.idx.command=\(.command | tojson)",
"mcp_servers.idx.args=\(.args | tojson)",
"mcp_servers.idx.env={\(.env | to_entries | map("\(.key)=\(.value | tojson)") | join(","))}"' <<< "$server_json")
CODEX_HOME="$work/codex" "${exec_prefix[@]}" codex exec --json --ephemeral \ CODEX_HOME="$work/codex" "${exec_prefix[@]}" codex exec --json --ephemeral \
--skip-git-repo-check -s read-only "${model[@]}" \ --skip-git-repo-check -s read-only "${model[@]}" \
-c "mcp_servers.idx.command=\"$launcher\"" -c 'mcp_servers.idx.args=[]' \ -c "${server[0]}" -c "${server[1]}" -c "${server[2]}" \
-c 'mcp_servers.idx.default_tools_approval_mode="approve"' \ -c 'mcp_servers.idx.default_tools_approval_mode="approve"' \
"$prompt" < /dev/null > "$out.raw" "$prompt" < /dev/null > "$out.raw"
jq -c 'select(.type=="item.completed" and .item.type=="mcp_tool_call" and .item.server=="idx") jq -c 'select(.type=="item.completed" and .item.type=="mcp_tool_call" and .item.server=="idx")
@ -175,7 +206,7 @@ setup_pi() {
mkdir -p "$work/pi" mkdir -p "$work/pi"
for file in auth.json models.json settings.json; do copy_if_present "$src/$file" "$work/pi"; done for file in auth.json models.json settings.json; do copy_if_present "$src/$file" "$work/pi"; done
# "direct" exposes each MCP tool by name (the default "codemode" wraps them). # "direct" exposes each MCP tool by name (the default "codemode" wraps them).
jq -n --arg cmd "$launcher" '{mcpServers: {idx: {command: $cmd, exposure: "direct"}}}' > "$work/pi/mcp.json" jq -n --argjson s "$server_json" '{mcpServers: {idx: ($s + {exposure: "direct"})}}' > "$work/pi/mcp.json"
} }
run_pi() { run_pi() {
local prompt="$1" out="$2" model=() local prompt="$1" out="$2" model=()
@ -193,17 +224,42 @@ setup_hermes() {
mkdir -p "$work/hermes" mkdir -p "$work/hermes"
copy_if_present "$src/auth.json" "$work/hermes" copy_if_present "$src/auth.json" "$work/hermes"
copy_if_present "$src/.env" "$work/hermes" copy_if_present "$src/.env" "$work/hermes"
# Model providers can be user plugins. A symlink: cleanup removes only the link.
[[ ! -d "$src/plugins" ]] || ln -s "$src/plugins" "$work/hermes/plugins"
local server
server="$(jq -c '. + {enabled: true}' <<< "$server_json")"
if jq -e 'type == "object"' "$src/config.yaml" >/dev/null 2>&1; then
# Hermes also writes config.yaml as JSON (e.g. in profiles).
jq --argjson s "$server" '{model, mcp_servers: {idx: $s}} | del(.. | nulls)' \
"$src/config.yaml" > "$work/hermes/config.yaml"
else
{ {
# The top-level `model:` block, up to the next top-level key. # The top-level `model:` block, up to the next top-level key.
[[ -f "$src/config.yaml" ]] && awk '/^[^[:space:]#]/ { in_model = /^model:/ } in_model' "$src/config.yaml" [[ -f "$src/config.yaml" ]] && awk '/^[^[:space:]#]/ { in_model = /^model:/ } in_model' "$src/config.yaml"
printf 'mcp_servers:\n idx:\n command: %s\n enabled: true\n' "$launcher" # JSON is valid YAML.
printf 'mcp_servers:\n idx: %s\n' "$server"
} > "$work/hermes/config.yaml" } > "$work/hermes/config.yaml"
fi
# A bare `npx` runs on Hermes' own Node, fetched into a fresh HERMES_HOME
# on first use (~20s); `hermes -z` waits only ~15s for MCP tools, so fetch
# it first or the first case runs without them.
if [[ -n "$npx_spec" ]]; then
HERMES_HOME="$work/hermes" "${exec_prefix[@]}" hermes pm install npm >/dev/null \
|| { echo "agent-smoke: hermes pm install npm failed" >&2; exit 2; }
fi
} }
run_hermes() { run_hermes() {
local prompt="$1" out="$2" model=() session local prompt="$1" out="$2" model=() session status=0
[[ -n "${IDX_AGENT_MODEL_HERMES:-}" ]] && model=(-m "$IDX_AGENT_MODEL_HERMES") [[ -n "${IDX_AGENT_MODEL_HERMES:-}" ]] && model=(-m "$IDX_AGENT_MODEL_HERMES")
HERMES_HOME="$work/hermes" "${exec_prefix[@]}" hermes -z "$prompt" "${model[@]}" \ # Only the idx toolset: with its terminal, Hermes can debug and repair a
--usage-file "$out.usage.json" < /dev/null > "$out.answer" # broken server mid-run and pass for the wrong reason.
HERMES_HOME="$work/hermes" "${exec_prefix[@]}" hermes -z "$prompt" "${model[@]}" -t idx \
--usage-file "$out.usage.json" < /dev/null > "$out.answer" || status=$?
# Hermes reports a dead server only as "Connection closed"; its stderr is here.
if [[ -f "$work/hermes/logs/mcp-stderr.log" ]]; then
cp "$work/hermes/logs/mcp-stderr.log" "$out.mcp-stderr"
fi
((status == 0)) || return "$status"
# -z prints only the answer; tool calls come from the session export. # -z prints only the answer; tool calls come from the session export.
session="$(jq -r '.session_id // empty' "$out.usage.json")" session="$(jq -r '.session_id // empty' "$out.usage.json")"
[[ -n "$session" ]] || return 1 [[ -n "$session" ]] || return 1
@ -219,8 +275,9 @@ run_hermes() {
setup_opencode() { setup_opencode() {
mkdir -p "$work/opencode/cwd" mkdir -p "$work/opencode/cwd"
jq -n --arg cmd "$launcher" \ jq -n --argjson s "$server_json" \
'{"$schema": "https://opencode.ai/config.json", mcp: {idx: {type: "local", command: [$cmd], enabled: true}}}' \ '{"$schema": "https://opencode.ai/config.json",
mcp: {idx: {type: "local", command: ([$s.command] + $s.args), environment: $s.env, enabled: true}}}' \
> "$work/opencode/cwd/opencode.json" > "$work/opencode/cwd/opencode.json"
} }
run_opencode() { run_opencode() {
@ -284,12 +341,15 @@ for agent in "${agents[@]}"; do
mkdir -p "$(dirname "$out")" mkdir -p "$(dirname "$out")"
: > "$out.calls"; : > "$out.answer" : > "$out.calls"; : > "$out.answer"
started=$SECONDS started=$SECONDS
if timeout "$timeout_secs" bash -c "set -eo pipefail; $(declare -f "run_$agent"); $(declare -p work launcher exec_prefix); run_$agent \"\$1\" \"\$2\"" _ "$prompt" "$out" 2> "$out.stderr"; then if timeout "$timeout_secs" bash -c "set -eo pipefail; $(declare -f "run_$agent"); $(declare -p work server_json exec_prefix); run_$agent \"\$1\" \"\$2\"" _ "$prompt" "$out" 2> "$out.stderr"; then
# Malformed agent output fails this run, not the whole matrix. # Malformed agent output fails this run, not the whole matrix.
failures="$(check_run "$case_json" "$out")" \ failures="$(check_run "$case_json" "$out")" \
|| failures="${failures:+$failures$'\n'}could not check the agent output (see $out.raw)" || failures="${failures:+$failures$'\n'}could not check the agent output (see $out.raw)"
else else
failures="agent exited with status $? (see $out.stderr)" status=$?
# Name the cause here: without --keep the output is deleted.
reason="$(grep -m1 -v '^[[:space:]]*$' "$out.answer" || tail -n1 "$out.stderr" || true)"
failures="agent exited with status $status${reason:+: ${reason:0:300}}"
fi fi
status=pass status=pass
[[ -z "$failures" ]] || status=fail [[ -z "$failures" ]] || status=fail

View file

@ -25,9 +25,8 @@ npx idx-cli --help # or run without installing
``` ```
The package downloads the checksum-verified release binary during The package downloads the checksum-verified release binary during
`postinstall`. pnpm 10+ and bun skip install scripts by default; allow them `postinstall`, or on first run when the package manager skips install scripts
with `pnpm add --global --allow-build=idx-cli idx-cli` or (npm 12 under `npx`, pnpm 10+, bun).
`bun add --global --trust idx-cli`.
</TabItem> </TabItem>
<TabItem label="Install script"> <TabItem label="Install script">