Skip to content

feat: add 'aidevops plugin' CLI commands for plugin management (t136.2)#759

Merged
alex-solovyev merged 1 commit intomainfrom
feature/t136.2-plugin-cli-commands
Feb 9, 2026
Merged

feat: add 'aidevops plugin' CLI commands for plugin management (t136.2)#759
alex-solovyev merged 1 commit intomainfrom
feature/t136.2-plugin-cli-commands

Conversation

@alex-solovyev
Copy link
Collaborator

@alex-solovyev alex-solovyev commented Feb 9, 2026

Summary

  • Add cmd_plugin() to aidevops.sh with 6 subcommands: add, list, update, enable, disable, remove
  • Plugin state stored in ~/.config/aidevops/plugins.json with namespace isolation
  • Namespace validation prevents collisions with core directories (custom, draft, scripts, tools, etc.)
  • Update plugins.md to reflect CLI availability

Details

Part of the Plugin System plan (t136). Follows t136.1 (plugin schema in .aidevops.json, PR #755).

Subcommands

Command Description
aidevops plugin add <repo-url> [--namespace <name>] [--branch <branch>] Clone plugin repo, register in plugins.json
aidevops plugin list Show installed plugins with status
aidevops plugin update [name] Update one or all enabled plugins
aidevops plugin enable <name> Enable plugin, redeploy files
aidevops plugin disable <name> Disable plugin, remove deployed files
aidevops plugin remove <name> Delete plugin files and config entry

Testing

  • bash -n aidevops.sh — syntax OK
  • aidevops plugin help — displays correctly
  • aidevops plugin list — shows "No plugins installed"
  • aidevops help — includes plugin in commands list
  • Reserved namespace rejection — correctly blocked
  • Invalid namespace characters — correctly blocked

Fixes: t136.2 ref:GH#729

Summary by CodeRabbit

  • New Features

    • Added plugin management command supporting add, list, update, enable, disable, and remove subcommands
    • Global plugin configuration now stored in ~/.config/aidevops/plugins.json with per-project awareness via .aidevops.json
    • Access full plugin CLI documentation via aidevops plugin help
  • Documentation

    • Updated plugin configuration guide with new storage structure and usage guidance

Add cmd_plugin() to aidevops.sh with add/list/update/enable/disable/remove
subcommands. Plugin state stored in ~/.config/aidevops/plugins.json.
Namespace validation prevents collisions with core directories.
Update plugins.md to reflect CLI availability.

Fixes: t136.2 ref:GH#729
@gemini-code-assist
Copy link

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Walkthrough

Plugin management documentation updated to describe global state storage in ~/.config/aidevops/plugins.json. New cmd_plugin() infrastructure added to aidevops.sh with subcommands (add, list, update, enable, disable, remove, help) for plugin lifecycle management, including namespace validation and git-based plugin deployment.

Changes

Cohort / File(s) Summary
Documentation Update
.agents/aidevops/plugins.md
Configuration section overhauled to document global plugin state storage at ~/.config/aidevops/plugins.json (auto-created) with per-project .aidevops.json plugin array. References CLI help for detailed usage.
Plugin Management System
aidevops.sh
New cmd_plugin() command set implementing add, list, update, enable, disable, remove, and help subcommands. Includes namespace validation (lowercase alphanumeric, hyphens; reserved name guards), JSON manifest operations, git-based cloning to ~/.aidevops/agents/<namespace>, and CLI routing integration.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Validator as Validator
    participant Git as Git Operations
    participant FileSystem as File System
    participant Config as Config Manager

    User->>Validator: aidevops plugin add --repo <url>
    Validator->>Validator: Derive/validate namespace
    Validator->>Validator: Check reserved names
    Validator-->>User: Namespace valid
    
    User->>Git: Clone plugin repository
    Git->>FileSystem: Extract to ~/.aidevops/agents/<namespace>
    Git-->>User: Clone complete
    
    User->>Config: Write plugin entry to plugins.json
    Config->>FileSystem: Update ~/.config/aidevops/plugins.json
    Config-->>User: Plugin registered
    
    User->>FileSystem: Enable flag set (enabled=true)
    FileSystem-->>User: Plugin ready
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested Reviewers

  • marcusquinn

Poem

🔌 Plugins dance in global state, ~/.config/aidevops.json's fate,
Add, list, update with grace, namespaces find their place,
Reserved names stand guard so true, git clones deploy anew,
DevOps automation takes flight, plugin management done right ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding plugin management CLI commands to aidevops.sh, which is the core feature introduced in this pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/t136.2-plugin-cli-commands

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Feb 9, 2026

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 50 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Mon Feb 9 04:40:24 UTC 2026: Code review monitoring started
Mon Feb 9 04:40:24 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 50

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 50
  • VULNERABILITIES: 0

Generated on: Mon Feb 9 04:40:27 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 9, 2026

@alex-solovyev alex-solovyev merged commit 435dd23 into main Feb 9, 2026
9 of 11 checks passed
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@aidevops.sh`:
- Around line 2169-2176: The argument-parsing case block (handling
--namespace|--ns, --branch|-b, --name|-n) can attempt to read $2 and shift 2
when a value is missing, causing a failure under set -euo pipefail; update each
flag branch to first verify that $# -ge 2 (or that $2 is non-empty) before
assigning namespace/branch/plugin_name and shifting, and if the value is missing
call print_error with a descriptive "missing value for <flag>" message and
return 1 so the script exits cleanly instead of failing on shift.
- Around line 2259-2269: The data rows are double-indented because the jq output
includes a leading two-space prefix; update the jq invocation that builds the
tab-separated line (the command reading from "$plugins_file" using jq -r
'.plugins[] | "  \(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"') to
remove the leading two spaces so it emits '\(.name)\t\(.namespace)\t\(.enabled
// true)\t\(.repo)'; keep the downstream while IFS=$'\t' read -r name ns enabled
repo and the printf that prints headers and rows unchanged so header and data
indentation match.
- Around line 2286-2298: The rm -rf calls are vulnerable when $ns (from
get_plugin_field) is empty and could delete $agents_dir; update every removal
site (the clone_dir removal in the update flow and the similar
remove/disable/bulk-update loops) to guard the variable using the parameter
expansion form so it fails fast (e.g., use ${ns:?} or ${clone_dir:?} when
constructing the path) before running rm -rf; ensure you reference the same
variables used in the code paths (ns, clone_dir, agents_dir) and apply the guard
consistently around the rm -rf invocations to prevent accidental deletion.
- Around line 2355-2363: The plugin JSON is being updated to set (.plugins[] |
select(.name == $n)).enabled = true (tmp_file/plugins_file) before verifying
deployment, so if git clone (git clone --branch "$target_branch" --depth 1
"$target_repo" "$agents_dir/$target_ns") fails (currently silenced by
2>/dev/null) the plugin appears enabled but has no files; change the flow in the
function to capture the git clone exit status and stderr, do not redirect errors
to /dev/null, and on clone failure log/print a clear warning including the git
error, revert the JSON change (move tmp_file backup or reset the .enabled flag
for target_name) or skip marking enabled until after a successful clone, and
ensure you remove any partially created directory (agents_dir/$target_ns) on
failure.
🧹 Nitpick comments (2)
aidevops.sh (2)

2094-2111: Missing jq availability check — every other command in this script validates it.

cmd_plugin unconditionally calls jq throughout (via get_plugin_field, list, add, remove, etc.), but never checks command -v jq. The rest of the script (e.g., cmd_repos, register_repo) consistently gates on jq availability and prints a helpful message. A missing jq here would produce cryptic "command not found" errors.

🔧 Add jq gate early in cmd_plugin
     # Ensure config dir exists
     mkdir -p "$CONFIG_DIR"
 
+    # jq is required for plugin management
+    if ! command -v jq &>/dev/null; then
+        print_error "jq is required for plugin management"
+        print_info "Install jq: https://jqlang.github.io/jq/download/"
+        return 1
+    fi
+
     # Initialize plugins.json if missing
     if [[ ! -f "$plugins_file" ]]; then

2219-2225: Inconsistent stderr handling: add leaks clone output, update suppresses it.

Line 2221 uses 2>&1 (merging stderr into stdout and displaying it), while the equivalent clone in the bulk-update path (line 2319) uses 2>/dev/null. The single-plugin update (line 2291) also uses 2>&1. Pick one approach — either show progress consistently or suppress consistently. Suppressing with 2>/dev/null and relying on exit-code messaging is cleaner given the print_success/print_error pattern already in use.

Comment on lines +2169 to +2176
while [[ $# -gt 0 ]]; do
case "$1" in
--namespace|--ns) namespace="$2"; shift 2 ;;
--branch|-b) branch="$2"; shift 2 ;;
--name|-n) plugin_name="$2"; shift 2 ;;
*) print_error "Unknown option: $1"; return 1 ;;
esac
done
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Option parsing will error out if a flag is passed without its value.

Under set -euo pipefail (line 8), if a user runs aidevops plugin add <url> --namespace (missing value), $2 is unset and shift 2 fails. Consider validating that $# ≥ 2 before consuming the value, or catch the missing-argument case.

🔧 Example guard
             while [[ $# -gt 0 ]]; do
                 case "$1" in
-                    --namespace|--ns) namespace="$2"; shift 2 ;;
-                    --branch|-b) branch="$2"; shift 2 ;;
-                    --name|-n) plugin_name="$2"; shift 2 ;;
+                    --namespace|--ns) [[ $# -ge 2 ]] || { print_error "Missing value for $1"; return 1; }; namespace="$2"; shift 2 ;;
+                    --branch|-b) [[ $# -ge 2 ]] || { print_error "Missing value for $1"; return 1; }; branch="$2"; shift 2 ;;
+                    --name|-n) [[ $# -ge 2 ]] || { print_error "Missing value for $1"; return 1; }; plugin_name="$2"; shift 2 ;;
                     *) print_error "Unknown option: $1"; return 1 ;;
                 esac
             done
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while [[ $# -gt 0 ]]; do
case "$1" in
--namespace|--ns) namespace="$2"; shift 2 ;;
--branch|-b) branch="$2"; shift 2 ;;
--name|-n) plugin_name="$2"; shift 2 ;;
*) print_error "Unknown option: $1"; return 1 ;;
esac
done
while [[ $# -gt 0 ]]; do
case "$1" in
--namespace|--ns) [[ $# -ge 2 ]] || { print_error "Missing value for $1"; return 1; }; namespace="$2"; shift 2 ;;
--branch|-b) [[ $# -ge 2 ]] || { print_error "Missing value for $1"; return 1; }; branch="$2"; shift 2 ;;
--name|-n) [[ $# -ge 2 ]] || { print_error "Missing value for $1"; return 1; }; plugin_name="$2"; shift 2 ;;
*) print_error "Unknown option: $1"; return 1 ;;
esac
done
🤖 Prompt for AI Agents
In `@aidevops.sh` around lines 2169 - 2176, The argument-parsing case block
(handling --namespace|--ns, --branch|-b, --name|-n) can attempt to read $2 and
shift 2 when a value is missing, causing a failure under set -euo pipefail;
update each flag branch to first verify that $# -ge 2 (or that $2 is non-empty)
before assigning namespace/branch/plugin_name and shifting, and if the value is
missing call print_error with a descriptive "missing value for <flag>" message
and return 1 so the script exits cleanly instead of failing on shift.

Comment on lines +2259 to +2269
printf " %-15s %-10s %-8s %s\n" "NAME" "NAMESPACE" "ENABLED" "REPO"
printf " %-15s %-10s %-8s %s\n" "----" "---------" "-------" "----"

jq -r '.plugins[] | " \(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$plugins_file" 2>/dev/null | \
while IFS=$'\t' read -r name ns enabled repo; do
local status_icon="yes"
if [[ "$enabled" == "false" ]]; then
status_icon="no"
fi
printf " %-15s %-10s %-8s %s\n" "$name" "$ns" "$status_icon" "$repo"
done
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

List output formatting: double indentation on data rows.

The jq template on line 2262 emits " \(.name)\t..." (2-space prefix), and then read assigns that prefixed string to $name. The subsequent printf on line 2268 adds another 2-space prefix, so data rows will be indented 4 spaces while headers (lines 2259-2260) are indented 2. Remove the leading spaces from the jq template.

🔧 Fix alignment
-            jq -r '.plugins[] | "  \(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$plugins_file" 2>/dev/null | \
+            jq -r '.plugins[] | "\(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$plugins_file" 2>/dev/null | \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
printf " %-15s %-10s %-8s %s\n" "NAME" "NAMESPACE" "ENABLED" "REPO"
printf " %-15s %-10s %-8s %s\n" "----" "---------" "-------" "----"
jq -r '.plugins[] | " \(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$plugins_file" 2>/dev/null | \
while IFS=$'\t' read -r name ns enabled repo; do
local status_icon="yes"
if [[ "$enabled" == "false" ]]; then
status_icon="no"
fi
printf " %-15s %-10s %-8s %s\n" "$name" "$ns" "$status_icon" "$repo"
done
printf " %-15s %-10s %-8s %s\n" "NAME" "NAMESPACE" "ENABLED" "REPO"
printf " %-15s %-10s %-8s %s\n" "----" "---------" "-------" "----"
jq -r '.plugins[] | "\(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$plugins_file" 2>/dev/null | \
while IFS=$'\t' read -r name ns enabled repo; do
local status_icon="yes"
if [[ "$enabled" == "false" ]]; then
status_icon="no"
fi
printf " %-15s %-10s %-8s %s\n" "$name" "$ns" "$status_icon" "$repo"
done
🤖 Prompt for AI Agents
In `@aidevops.sh` around lines 2259 - 2269, The data rows are double-indented
because the jq output includes a leading two-space prefix; update the jq
invocation that builds the tab-separated line (the command reading from
"$plugins_file" using jq -r '.plugins[] | "  \(.name)\t\(.namespace)\t\(.enabled
// true)\t\(.repo)"') to remove the leading two spaces so it emits
'\(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)'; keep the downstream
while IFS=$'\t' read -r name ns enabled repo and the printf that prints headers
and rows unchanged so header and data indentation match.

Comment on lines +2286 to +2298
fi

print_info "Updating plugin '$target'..."
local clone_dir="$agents_dir/$ns"
rm -rf "$clone_dir"
if git clone --branch "$branch_name" --depth 1 "$repo" "$clone_dir" 2>&1; then
rm -rf "$clone_dir/.git"
print_success "Plugin '$target' updated"
else
print_error "Failed to update plugin '$target'"
return 1
fi
else
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard rm -rf against empty namespace to prevent catastrophic deletion.

If get_plugin_field returns an empty string (e.g., jq not installed or JSON parse failure), $ns is empty and rm -rf "$agents_dir/" would wipe the entire agents directory. The same pattern appears in the bulk-update loop (line 2318), disable (line 2389), and remove (line 2411). The static analysis tool flagged lines 2389 and 2411 with SC2115, but the issue is present in all four rm -rf sites.

🛡️ Proposed fix — use ${var:?} guards on all rm -rf calls
                 print_info "Updating plugin '$target'..."
                 local clone_dir="$agents_dir/$ns"
-                rm -rf "$clone_dir"
+                rm -rf "${clone_dir:?}"
                 if git clone --branch "$branch_name" --depth 1 "$repo" "$clone_dir" 2>&1; then
                     local pdir="$agents_dir/$pns"
-                    rm -rf "$pdir"
+                    rm -rf "${pdir:?}"
                     if git clone --branch "$pbranch" --depth 1 "$prepo" "$pdir" 2>/dev/null; then
             # Remove deployed files
             if [[ -d "$agents_dir/$target_ns" ]]; then
-                rm -rf "$agents_dir/$target_ns"
+                rm -rf "${agents_dir:?}/${target_ns:?}"
             fi

(Apply to both the disable block at line 2389 and the remove block at line 2411.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fi
print_info "Updating plugin '$target'..."
local clone_dir="$agents_dir/$ns"
rm -rf "$clone_dir"
if git clone --branch "$branch_name" --depth 1 "$repo" "$clone_dir" 2>&1; then
rm -rf "$clone_dir/.git"
print_success "Plugin '$target' updated"
else
print_error "Failed to update plugin '$target'"
return 1
fi
else
fi
print_info "Updating plugin '$target'..."
local clone_dir="$agents_dir/$ns"
rm -rf "${clone_dir:?}"
if git clone --branch "$branch_name" --depth 1 "$repo" "$clone_dir" 2>&1; then
rm -rf "$clone_dir/.git"
print_success "Plugin '$target' updated"
else
print_error "Failed to update plugin '$target'"
return 1
fi
else
🤖 Prompt for AI Agents
In `@aidevops.sh` around lines 2286 - 2298, The rm -rf calls are vulnerable when
$ns (from get_plugin_field) is empty and could delete $agents_dir; update every
removal site (the clone_dir removal in the update flow and the similar
remove/disable/bulk-update loops) to guard the variable using the parameter
expansion form so it fails fast (e.g., use ${ns:?} or ${clone_dir:?} when
constructing the path) before running rm -rf; ensure you reference the same
variables used in the code paths (ns, clone_dir, agents_dir) and apply the guard
consistently around the rm -rf invocations to prevent accidental deletion.

Comment on lines +2355 to +2363
local tmp_file="${plugins_file}.tmp"
jq --arg n "$target_name" '(.plugins[] | select(.name == $n)).enabled = true' "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"

# Deploy if not already present
if [[ ! -d "$agents_dir/$target_ns" ]]; then
print_info "Deploying plugin '$target_name'..."
if git clone --branch "$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns" 2>/dev/null; then
rm -rf "$agents_dir/$target_ns/.git"
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent failure when enable clone fails — plugin marked enabled but not deployed.

If git clone fails on line 2361, the error is suppressed (2>/dev/null), the function continues, and the plugin is reported as "enabled" (line 2366) despite having no deployed files. Consider checking the clone exit code and warning the user.

🔧 Proposed fix
             # Deploy if not already present
             if [[ ! -d "$agents_dir/$target_ns" ]]; then
                 print_info "Deploying plugin '$target_name'..."
-                if git clone --branch "$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns" 2>/dev/null; then
+                if git clone --branch "$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns" 2>&1; then
                     rm -rf "$agents_dir/$target_ns/.git"
+                else
+                    print_error "Failed to deploy plugin '$target_name' — enabled in config but files not deployed"
+                    return 1
                 fi
             fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local tmp_file="${plugins_file}.tmp"
jq --arg n "$target_name" '(.plugins[] | select(.name == $n)).enabled = true' "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"
# Deploy if not already present
if [[ ! -d "$agents_dir/$target_ns" ]]; then
print_info "Deploying plugin '$target_name'..."
if git clone --branch "$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns" 2>/dev/null; then
rm -rf "$agents_dir/$target_ns/.git"
fi
local tmp_file="${plugins_file}.tmp"
jq --arg n "$target_name" '(.plugins[] | select(.name == $n)).enabled = true' "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"
# Deploy if not already present
if [[ ! -d "$agents_dir/$target_ns" ]]; then
print_info "Deploying plugin '$target_name'..."
if git clone --branch "$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns" 2>&1; then
rm -rf "$agents_dir/$target_ns/.git"
else
print_error "Failed to deploy plugin '$target_name' — enabled in config but files not deployed"
return 1
fi
🤖 Prompt for AI Agents
In `@aidevops.sh` around lines 2355 - 2363, The plugin JSON is being updated to
set (.plugins[] | select(.name == $n)).enabled = true (tmp_file/plugins_file)
before verifying deployment, so if git clone (git clone --branch
"$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns") fails
(currently silenced by 2>/dev/null) the plugin appears enabled but has no files;
change the flow in the function to capture the git clone exit status and stderr,
do not redirect errors to /dev/null, and on clone failure log/print a clear
warning including the git error, revert the JSON change (move tmp_file backup or
reset the .enabled flag for target_name) or skip marking enabled until after a
successful clone, and ensure you remove any partially created directory
(agents_dir/$target_ns) on failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant