diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash index a52f3a5..2f60e47 100644 --- a/completions/mcpctl.bash +++ b/completions/mcpctl.bash @@ -46,7 +46,7 @@ _mcpctl() { # If completing the --project value if [[ "$prev" == "--project" ]]; then local names - names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null) + names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[][].name' 2>/dev/null) COMPREPLY=($(compgen -W "$names" -- "$cur")) return fi @@ -55,7 +55,7 @@ _mcpctl() { _mcpctl_resource_names() { local rt="$1" if [[ -n "$rt" ]]; then - mcpctl get "$rt" -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null + mcpctl get "$rt" -o json 2>/dev/null | jq -r '.[][].name' 2>/dev/null fi } diff --git a/completions/mcpctl.fish b/completions/mcpctl.fish index a5494ed..164b771 100644 --- a/completions/mcpctl.fish +++ b/completions/mcpctl.fish @@ -72,12 +72,12 @@ function __mcpctl_resource_names if test -z "$resource" return end - mcpctl get $resource -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null + mcpctl get $resource -o json 2>/dev/null | jq -r '.[][].name' 2>/dev/null end # Fetch project names for --project value function __mcpctl_project_names - mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null + mcpctl get projects -o json 2>/dev/null | jq -r '.[][].name' 2>/dev/null end # --project value completion diff --git a/pr.sh b/pr.sh index ab64885..e645938 100755 --- a/pr.sh +++ b/pr.sh @@ -1,11 +1,18 @@ #!/usr/bin/env bash -# Usage: source .env && bash pr.sh "PR title" "PR body" -# Requires GITEA_TOKEN in environment +# Usage: bash pr.sh "PR title" "PR body" +# Loads GITEA_TOKEN from .env automatically set -euo pipefail -GITEA_URL="http://10.0.0.194:3012" -REPO="michal/mcpctl" +# Load .env if GITEA_TOKEN not already exported +if [ -z "${GITEA_TOKEN:-}" ] && [ -f .env ]; then + set -a + source .env + set +a +fi + +GITEA_URL="${GITEA_URL:-http://10.0.0.194:3012}" +REPO="${GITEA_OWNER:-michal}/mcpctl" TITLE="${1:?Usage: pr.sh [body]}" BODY="${2:-}" @@ -18,7 +25,7 @@ if [ "$HEAD" = "$BASE" ]; then fi if [ -z "${GITEA_TOKEN:-}" ]; then - echo "Error: GITEA_TOKEN not set. Run: source .env" >&2 + echo "Error: GITEA_TOKEN not set and .env not found" >&2 exit 1 fi diff --git a/src/cli/tests/completions.test.ts b/src/cli/tests/completions.test.ts index df23bd2..5a51993 100644 --- a/src/cli/tests/completions.test.ts +++ b/src/cli/tests/completions.test.ts @@ -72,16 +72,19 @@ describe('fish completions', () => { } }); - it('resource name functions use jq (not regex) to avoid matching nested name fields', () => { - // Regex like "name":\s*"..." on JSON matches nested server names inside project objects. - // Must use jq -r '.[].name' to extract only top-level names. + it('resource name functions use jq .[][].name to unwrap wrapped JSON and avoid nested matches', () => { + // API returns { "resources": [...] } not [...], so .[].name fails silently. + // Must use .[][].name to unwrap the outer object then iterate the array. + // Also must not use string match regex which matches nested name fields. const resourceNamesFn = fishFile.match(/function __mcpctl_resource_names[\s\S]*?^end/m)?.[0] ?? ''; const projectNamesFn = fishFile.match(/function __mcpctl_project_names[\s\S]*?^end/m)?.[0] ?? ''; - expect(resourceNamesFn, '__mcpctl_resource_names must use jq').toContain("jq -r '.[].name'"); + expect(resourceNamesFn, '__mcpctl_resource_names must use jq .[][].name').toContain("jq -r '.[][].name'"); expect(resourceNamesFn, '__mcpctl_resource_names must not use string match on name').not.toMatch(/string match.*"name"/); + // Guard against .[].name (single bracket) which fails on wrapped JSON + expect(resourceNamesFn, '__mcpctl_resource_names must not use .[].name (needs .[][].name)').not.toMatch(/jq.*'\.\[\]\.name'/); - expect(projectNamesFn, '__mcpctl_project_names must use jq').toContain("jq -r '.[].name'"); + expect(projectNamesFn, '__mcpctl_project_names must use jq .[][].name').toContain("jq -r '.[][].name'"); expect(projectNamesFn, '__mcpctl_project_names must not use string match on name').not.toMatch(/string match.*"name"/); }); @@ -126,9 +129,11 @@ describe('bash completions', () => { expect(bashFile).toContain('--project'); }); - it('resource name function uses jq (not grep regex) to avoid matching nested name fields', () => { + it('resource name function uses jq .[][].name to unwrap wrapped JSON and avoid nested matches', () => { const fnMatch = bashFile.match(/_mcpctl_resource_names\(\)[\s\S]*?\n\s*\}/)?.[0] ?? ''; - expect(fnMatch, '_mcpctl_resource_names must use jq').toContain("jq -r '.[].name'"); + expect(fnMatch, '_mcpctl_resource_names must use jq .[][].name').toContain("jq -r '.[][].name'"); expect(fnMatch, '_mcpctl_resource_names must not use grep on name').not.toMatch(/grep.*"name"/); + // Guard against .[].name (single bracket) which fails on wrapped JSON + expect(fnMatch, '_mcpctl_resource_names must not use .[].name (needs .[][].name)').not.toMatch(/jq.*'\.\[\]\.name'/); }); });