From e2c54bfc5c596c2741e3067e62568fbf713ae394 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 23 Feb 2026 19:23:21 +0000 Subject: [PATCH] fix: use jq for completion name extraction to avoid nested matches The regex "name":\s*"..." on JSON matched nested server names inside project objects, mixing resource types in completions. Switch to jq -r '.[].name' for proper top-level extraction. Add jq as RPM dependency. Add pr.sh for PR creation via Gitea API. Co-Authored-By: Claude Opus 4.6 --- completions/mcpctl.bash | 6 ++-- completions/mcpctl.fish | 7 ++--- nfpm.yaml | 2 ++ pr.sh | 48 +++++++++++++++++++++++++++++++ src/cli/tests/completions.test.ts | 19 ++++++++++++ 5 files changed, 75 insertions(+), 7 deletions(-) create mode 100755 pr.sh diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash index 00811fb..a52f3a5 100644 --- a/completions/mcpctl.bash +++ b/completions/mcpctl.bash @@ -46,16 +46,16 @@ _mcpctl() { # If completing the --project value if [[ "$prev" == "--project" ]]; then local names - names=$(mcpctl get projects -o json 2>/dev/null | grep -oP '"name":\s*"\K[^"]+') + names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null) COMPREPLY=($(compgen -W "$names" -- "$cur")) return fi - # Fetch resource names dynamically + # Fetch resource names dynamically (jq extracts only top-level names) _mcpctl_resource_names() { local rt="$1" if [[ -n "$rt" ]]; then - mcpctl get "$rt" -o json 2>/dev/null | grep -oP '"name":\s*"\K[^"]+' + 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 24c8b15..a5494ed 100644 --- a/completions/mcpctl.fish +++ b/completions/mcpctl.fish @@ -66,19 +66,18 @@ function __mcpctl_get_resource_type end end -# Fetch resource names dynamically from the API +# Fetch resource names dynamically from the API (jq extracts only top-level names) function __mcpctl_resource_names set -l resource (__mcpctl_get_resource_type) if test -z "$resource" return end - # Use mcpctl to fetch names (quick JSON parse with string manipulation) - mcpctl get $resource -o json 2>/dev/null | string match -rg '"name":\s*"([^"]+)"' + 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 | string match -rg '"name":\s*"([^"]+)"' + mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null end # --project value completion diff --git a/nfpm.yaml b/nfpm.yaml index 430c0d4..594c236 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -5,6 +5,8 @@ release: "1" maintainer: michal description: kubectl-like CLI for managing MCP servers license: MIT +depends: + - jq contents: - src: ./dist/mcpctl dst: /usr/bin/mcpctl diff --git a/pr.sh b/pr.sh new file mode 100755 index 0000000..ab64885 --- /dev/null +++ b/pr.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Usage: source .env && bash pr.sh "PR title" "PR body" +# Requires GITEA_TOKEN in environment + +set -euo pipefail + +GITEA_URL="http://10.0.0.194:3012" +REPO="michal/mcpctl" + +TITLE="${1:?Usage: pr.sh [body]}" +BODY="${2:-}" +BASE="${3:-main}" +HEAD=$(git rev-parse --abbrev-ref HEAD) + +if [ "$HEAD" = "$BASE" ]; then + echo "Error: already on $BASE, switch to a feature branch first" >&2 + exit 1 +fi + +if [ -z "${GITEA_TOKEN:-}" ]; then + echo "Error: GITEA_TOKEN not set. Run: source .env" >&2 + exit 1 +fi + +# Push if needed +if ! git rev-parse --verify "origin/$HEAD" &>/dev/null; then + git push -u origin "$HEAD" +else + git push +fi + +# Create PR +RESPONSE=$(curl -s -X POST "$GITEA_URL/api/v1/repos/$REPO/pulls" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg t "$TITLE" --arg b "$BODY" --arg h "$HEAD" --arg base "$BASE" \ + '{title: $t, body: $b, head: $h, base: $base}')") + +PR_NUM=$(echo "$RESPONSE" | jq -r '.number // empty') +PR_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty') + +if [ -z "$PR_NUM" ]; then + echo "Error creating PR:" >&2 + echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" >&2 + exit 1 +fi + +echo "PR #$PR_NUM: https://mysources.co.uk/$REPO/pulls/$PR_NUM" diff --git a/src/cli/tests/completions.test.ts b/src/cli/tests/completions.test.ts index b2a513c..df23bd2 100644 --- a/src/cli/tests/completions.test.ts +++ b/src/cli/tests/completions.test.ts @@ -72,6 +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. + 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 not use string match on name').not.toMatch(/string match.*"name"/); + + expect(projectNamesFn, '__mcpctl_project_names must use jq').toContain("jq -r '.[].name'"); + expect(projectNamesFn, '__mcpctl_project_names must not use string match on name').not.toMatch(/string match.*"name"/); + }); + it('non-project commands do not show with --project', () => { const nonProjectCmds = ['status', 'login', 'logout', 'config', 'apply', 'backup', 'restore']; const lines = fishFile.split('\n').filter((l) => l.startsWith('complete') && l.includes('-a ')); @@ -112,4 +125,10 @@ describe('bash completions', () => { it('defines --project option', () => { expect(bashFile).toContain('--project'); }); + + it('resource name function uses jq (not grep regex) to avoid matching nested name fields', () => { + 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 not use grep on name').not.toMatch(/grep.*"name"/); + }); }); -- 2.49.1