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'/);
});
});