Skip to content

Add packages command to catalog Sparkdock-managed packages by category#254

Open
Copilot wants to merge 7 commits intomasterfrom
copilot/add-package-catalog-command
Open

Add packages command to catalog Sparkdock-managed packages by category#254
Copilot wants to merge 7 commits intomasterfrom
copilot/add-package-catalog-command

Conversation

Copy link
Contributor

Copilot AI commented Oct 16, 2025

Overview

Adds a new sjust packages command that allows users to easily discover and catalog all Sparkdock-managed packages directly from the CLI, organized by category with descriptions and optional URLs.

Problem

Users had no way to view which packages are managed by Sparkdock without opening the codebase and searching through YAML configuration files. This made it difficult to:

  • Discover what packages are available
  • Find packages by name or category
  • Understand what each package does
  • Access project homepages or repositories

Solution

Implemented a simple package catalog system:

1. Package Metadata

Extended config/packages/all-packages.yml to include metadata inline with each package:

  • Category: Organizes packages into 19 topics (Container Tools, Cloud Tools, Development Tools, Security, etc.)
  • Description: Clear explanation of what each package does
  • URL: (Optional) Link to the project's git repository or homepage

Each package now has a structured format:

cask_packages:
  - name: docker-desktop
    category: Container Tools
    description: Docker Desktop - Container development platform
    url: https://www.docker.com/products/docker-desktop/

2. Package Listing Script

Added sjust/libs/list-packages.sh that:

  • Parses package entries from configuration file
  • Retrieves metadata for each package
  • Formats output as a clean table
  • Supports filtering by package name
  • Sorts output by category for easy browsing
  • Displays optional URLs when available

3. Sjust Commands

Added two command variants:

  • sjust packages [filter] - Short alias (matches issue suggestion)
  • sjust sparkdock-packages [filter] - Long form (consistent with other commands)

4. Ansible Integration

Updated ansible/macos/macos/base.yml to extract package names from the new structure using map(attribute='name') filter.

5. Custom Instructions

Updated .github/copilot-instructions.md to document the requirement that all new packages must include name, category, and description fields, with url being optional. This ensures future contributors follow the correct pattern when adding packages.

Usage

# List all packages
sjust packages

# Filter by package name
sjust packages docker
sjust packages kubernetes

Example Output

Package                             | Category                  | Description                                   | URL
------------------------------------+---------------------------+-----------------------------------------------+---------------------------------------------------
docker-desktop                      | Container Tools           | Docker Desktop - Container development pla... | https://www.docker.com/products/docker-desktop/
kubernetes-cli                      | Container Tools           | kubectl - Kubernetes command line tool        | https://github.com/kubernetes/kubernetes
awscli                              | Cloud Tools               | AWS CLI - Amazon Web Services command line... | https://github.com/aws/aws-cli

Total packages: 59

Categories

Packages are organized into 19 categories:

  • Browsers
  • Cloud Tools
  • Code Editors
  • Communication
  • Container Tools
  • Development Tools
  • Fonts
  • Image Processing
  • Keyboard Tools
  • Package Managers
  • Productivity
  • Programming Languages
  • Security
  • Shell Utilities
  • System Monitoring
  • System Utilities
  • Terminal Emulators
  • Terminal Utilities
  • Text Processing

Testing

Added comprehensive test coverage in .github/workflows/test-sjust.yml:

  • Command syntax validation (dry-run)
  • Full package listing verification
  • Package filtering functionality
  • Table format correctness
  • Short alias testing

Documentation

  • Updated README.md with usage examples and Package Management section
  • Updated CHANGELOG.md with feature entry
  • Updated .github/copilot-instructions.md with package metadata requirements including the optional URL field
  • All code follows project standards (shellcheck validation, proper formatting)

Fixes #253

Original prompt

This section details on the original issue you should resolve

<issue_title>Have a way to catalog the installed packages from sparkdock divided by topic</issue_title>
<issue_description>At the moment, users don't have a way, from the CLI, SparkDock, or Sjust, to know which packages are installed unless they open this codebase and search inside it.

It would be nice to have a command like this:

just packages <package-name>

| pkg | category | description |
|-----|----------|-------------|
|     |          |             |
|     |          |             |
|     |          |             |

We could use brew to query package information.</issue_description>

Comments on the Issue (you are @copilot in this section)

Fixes #253


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.


PR Type

Enhancement, Documentation


Description

  • Add packages command to list Sparkdock-managed packages with categories

  • Extend package configuration with inline metadata (category, description)

  • Implement filtering by package name

  • Add CI tests for package listing functionality


Diagram Walkthrough

flowchart LR
  config["all-packages.yml<br/>with metadata"] --> script["list-packages.sh<br/>parser"]
  script --> cmd1["sjust packages"]
  script --> cmd2["sjust sparkdock-packages"]
  cmd1 --> output["Formatted table<br/>with filtering"]
  cmd2 --> output
Loading

File Walkthrough

Relevant files
Enhancement
2 files
list-packages.sh
Add package listing script with YAML parsing and filtering
+93/-0   
00-default.just
Add sparkdock-packages and packages commands                         
+14/-2   
Configuration changes
1 files
all-packages.yml
Restructure packages with category and description metadata
+177/-60
Bug fix
1 files
base.yml
Update package extraction to use name attribute                   
+3/-3     
Tests
1 files
test-sjust.yml
Add CI tests for packages command functionality                   
+39/-0   
Documentation
2 files
README.md
Document package management commands with examples             
+26/-4   
CHANGELOG.md
Add changelog entries for packages command feature             
+4/-0     

Copilot AI and others added 3 commits October 16, 2025 15:43
Co-authored-by: paolomainardi <8747+paolomainardi@users.noreply.github.com>
Co-authored-by: paolomainardi <8747+paolomainardi@users.noreply.github.com>
Co-authored-by: paolomainardi <8747+paolomainardi@users.noreply.github.com>
Copilot AI changed the title [WIP] Add command to catalog installed packages by topic Add packages command to catalog Sparkdock-managed packages by category Oct 16, 2025
Copilot AI requested a review from paolomainardi October 16, 2025 15:51
@paolomainardi
Copy link
Member

@copilot we can make it way simpler than this. We can extend the all-packages.yml file to include static metadata, get rid of the script that uses brew, for now keep it simple.

Co-authored-by: paolomainardi <8747+paolomainardi@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot we can make it way simpler than this. We can extend the all-packages.yml file to include static metadata, get rid of the script that uses brew, for now keep it simple.

Simplified as requested. Changes in commit 10a7e2c:

What changed:

  • Merged metadata directly into all-packages.yml (packages now have name, category, and description fields)
  • Removed separate package-metadata.yml file
  • Removed brew installation checking (no more "Installed" column)
  • Simplified script (~90 lines vs ~156 lines)
  • Updated Ansible playbook to extract package names with map(attribute='name')

New structure:

cask_packages:
  - name: docker-desktop
    category: Container Tools
    description: Docker Desktop - Container development platform

Output now shows 3 columns instead of 4: Package | Category | Description

@paolomainardi paolomainardi marked this pull request as ready for review October 16, 2025 16:43
Copilot AI review requested due to automatic review settings October 16, 2025 16:43
@paolomainardi
Copy link
Member

@copilot, add to the custom instructions that any new package added must include the category and a description.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Co-authored-by: paolomainardi <8747+paolomainardi@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot, add to the custom instructions that any new package added must include the category and a description.

Added to .github/copilot-instructions.md in commit 7c8f579.

Updated the Package Management Pattern section to:

  • Show the new YAML structure with name, category, and description fields
  • Add explicit requirement that all new packages must include these three fields
  • Explain that the metadata is used by the sjust packages command

@paolomainardi
Copy link
Member

It would be nice to have an url field on the package definition, to point to the git repo or the project page if it is closed source.

@copilot

Co-authored-by: paolomainardi <8747+paolomainardi@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

It would be nice to have an url field on the package definition, to point to the git repo or the project page if it is closed source.

@copilot

Added optional url field in commit 9e3d7f8.

Changes:

  • Updated package definitions to include optional url field (added URLs to most packages)
  • Modified script to parse and display URLs in a 4th column
  • Updated custom instructions to document the optional url field
  • Updated README to show the new column in example output

Example output:

Package         | Category        | Description              | URL
----------------|-----------------|--------------------------|---------------------------
docker-desktop  | Container Tools | Docker Desktop - Cont... | https://www.docker.com/...
kubernetes-cli  | Container Tools | kubectl - Kubernetes ... | https://github.com/kuber...

The URL field is optional - packages without URLs will show an empty column.

@paolomainardi
Copy link
Member

@claude review this MR

@paolomainardi paolomainardi requested a review from Copilot October 19, 2025 17:21
@paolomainardi
Copy link
Member

/describe

@paolomainardi
Copy link
Member

/improve

@paolomainardi
Copy link
Member

/review

@sparkfabrik-ai-bot
Copy link

Preparing PR description...

@sparkfabrik-ai-bot
Copy link

Generating PR code suggestions

Work in progress ...

@sparkfabrik-ai-bot
Copy link

Preparing review...

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Comment on lines +30 to +35
# Check if next line has URL
url=""
if (getline > 0 && $0 ~ /^[[:space:]]+url:/) {
gsub(/^[[:space:]]+url: /, "")
gsub(/"/, "")
url=$0
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

The unconditional getline used to check for a url line consumes the next package's - name: line when the current package has no url, causing the following package (e.g., btop after bc, openssl@3 after watch) to be skipped entirely. Avoid consuming the next record unless it is a url line (e.g., read into a temporary variable and only advance if it matches, or switch to a YAML-aware tool like yq). Example fix in awk: read the next line into temp; if it matches url parse it; otherwise store it and use NR-- with nextfile logic or restructure to process line-by-line outside the record block.

Suggested change
# Check if next line has URL
url=""
if (getline > 0 && $0 ~ /^[[:space:]]+url:/) {
gsub(/^[[:space:]]+url: /, "")
gsub(/"/, "")
url=$0
# Check if next line has URL, but do not consume if not present
url=""
nextline = ""
if (getline nextline > 0) {
if (nextline ~ /^[[:space:]]+url:/) {
gsub(/^[[:space:]]+url: /, "", nextline)
gsub(/"/, "", nextline)
url=nextline
} else {
# Not a url line, so push back for next record
NR--
}

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +55
# Skip if filter is set and doesn't match
if [[ -n "${filter_package}" ]] && [[ ! "${package}" =~ ${filter_package} ]]; then
continue
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

Using the unquoted user-supplied filter as a regex risks unintended matches or regex syntax errors if special characters are provided; for simple substring matching replace with a safer pattern like: if [[ -n "${filter_package}" ]] && [[ "${package}" != "${filter_package}" ]]; then continue; (quote the variable to avoid word-splitting for patterns containing spaces).

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +72
# Main function
main() {
local filter_package="${1:-}"

# Collect all packages
declare -a all_rows=()

# Process cask packages
while IFS='|' read -r package category description url; do
[[ -z "${package}" ]] && continue

# Skip if filter is set and doesn't match
if [[ -n "${filter_package}" ]] && [[ ! "${package}" =~ ${filter_package} ]]; then
continue
fi

all_rows+=("${package}|${category}|${description}|${url}")
done < <(get_yaml_package_entries "${PACKAGES_YML}" "cask_packages")

# Process homebrew packages
while IFS='|' read -r package category description url; do
[[ -z "${package}" ]] && continue

# Skip if filter is set and doesn't match
if [[ -n "${filter_package}" ]] && [[ ! "${package}" =~ ${filter_package} ]]; then
continue
fi

all_rows+=("${package}|${category}|${description}|${url}")
done < <(get_yaml_package_entries "${PACKAGES_YML}" "homebrew_packages")

Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

[nitpick] The two loops duplicate identical logic differing only by the key; factor into a small function (e.g., process_section "cask_packages"; process_section "homebrew_packages") to reduce duplication and ease future changes.

Suggested change
# Main function
main() {
local filter_package="${1:-}"
# Collect all packages
declare -a all_rows=()
# Process cask packages
while IFS='|' read -r package category description url; do
[[ -z "${package}" ]] && continue
# Skip if filter is set and doesn't match
if [[ -n "${filter_package}" ]] && [[ ! "${package}" =~ ${filter_package} ]]; then
continue
fi
all_rows+=("${package}|${category}|${description}|${url}")
done < <(get_yaml_package_entries "${PACKAGES_YML}" "cask_packages")
# Process homebrew packages
while IFS='|' read -r package category description url; do
[[ -z "${package}" ]] && continue
# Skip if filter is set and doesn't match
if [[ -n "${filter_package}" ]] && [[ ! "${package}" =~ ${filter_package} ]]; then
continue
fi
all_rows+=("${package}|${category}|${description}|${url}")
done < <(get_yaml_package_entries "${PACKAGES_YML}" "homebrew_packages")
# Helper function to process a package section
process_section() {
local section_key="$1"
local filter_package="$2"
local -n rows_ref="$3"
while IFS='|' read -r package category description url; do
[[ -z "${package}" ]] && continue
# Skip if filter is set and doesn't match
if [[ -n "${filter_package}" ]] && [[ ! "${package}" =~ ${filter_package} ]]; then
continue
fi
rows_ref+=("${package}|${category}|${description}|${url}")
done < <(get_yaml_package_entries "${PACKAGES_YML}" "${section_key}")
}
# Main function
main() {
local filter_package="${1:-}"
# Collect all packages
declare -a all_rows=()
# Process both sections
process_section "cask_packages" "${filter_package}" all_rows
process_section "homebrew_packages" "${filter_package}" all_rows

Copilot uses AI. Check for mistakes.
url: https://cloud.google.com/sdk
- name: applite
category: System Utilities
description: ApplitE - Third-party app installer for macOS
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

Corrected spelling/capitalization of 'ApplitE' to 'Applite' to match the project name.

Suggested change
description: ApplitE - Third-party app installer for macOS
description: Applite - Third-party app installer for macOS

Copilot uses AI. Check for mistakes.
exit 1
fi

# Verify table format (3 columns: Package, Category, Description)
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

Comment is outdated: the table includes four columns (Package, Category, Description, URL); update the comment to reflect the current format to avoid confusion.

Suggested change
# Verify table format (3 columns: Package, Category, Description)
# Verify table format (4 columns: Package, Category, Description, URL)

Copilot uses AI. Check for mistakes.
@paolomainardi
Copy link
Member

@claude, review this MR and let me know what you think about this, what improvements we can make.

@claude
Copy link

claude bot commented Dec 13, 2025

Claude finished @paolomainardi's task in 1m 30s —— View job


PR #254 Review - Package Catalog Command

Todo List:

  • Read key implementation files
  • Analyze the code for issues
  • Review existing automated feedback
  • Provide comprehensive review feedback

Overall Assessment

This is a well-executed feature that elegantly solves the problem of package discovery. The implementation is clean, follows project standards, and includes comprehensive testing. However, there are several critical bugs that need to be addressed before merging.

Strengths

  1. Smart Design Decision: Embedding metadata directly into all-packages.yml is much simpler than the brew-based approach and aligns perfectly with the "keep it simple" philosophy.

  2. Comprehensive Coverage: All 59 packages now have proper metadata with well-organized categories and clear descriptions.

  3. Good Test Coverage: The CI tests validate both syntax and functionality, including filtering and table format.

  4. Documentation: README, CHANGELOG, and copilot instructions are all properly updated.

  5. Code Standards: Follows shell script best practices with proper error handling (set -euo pipefail).

🐛 Critical Issues

1. AWK getline Bug - CRITICAL (⭐ Highest Priority)

File: sjust/libs/list-packages.sh:32-36

Problem: The getline call at line 32 unconditionally consumes the next line to check for a URL. When a package doesn't have a URL, this consumes and discards the next package's - name: line, causing packages to be completely skipped from the output.

Impact: Looking at the YAML:

  • Line 116-118: bc has no URL
  • Line 119-122: btop (next package) will be skipped
  • Line 188-190: watch has no URL
  • Line 191-193: openssl@3 (next package) will be skipped

Evidence from YAML:

  - name: bc
    category: System Utilities
    description: bc - Arbitrary precision calculator
  - name: btop      # ← This gets consumed and lost when processing bc
    category: System Monitoring
    description: btop - Resource monitor with terminal UI

Fix: You need to peek at the next line without consuming it unless it's actually a URL line. Here's the corrected AWK logic:

# Check if next line has URL (peek without consuming)
url=""
if ((getline nextline) > 0) {
    if (nextline ~ /^[[:space:]]+url:/) {
        gsub(/^[[:space:]]+url: /, "", nextline)
        gsub(/"/, "", nextline)
        url = nextline
    } else {
        # Not a URL line - we need to "put it back" for next iteration
        # Save it in a variable and check at start of next iteration
        saved_line = nextline
        has_saved = 1
    }
}

A better approach might be to restructure the AWK to process line-by-line and maintain state, or use a YAML-aware tool like yq.


2. Unsafe Regex Pattern (🔒 Security/Reliability)

File: sjust/libs/list-packages.sh:54 and sjust/libs/list-packages.sh:66

Problem: The user-supplied filter is used directly in a bash regex match without sanitization:

if [[ ! "${package}" =~ ${filter_package} ]]; then

Risk:

  • Special regex characters (., *, [, etc.) in user input cause unintended matches
  • Invalid regex syntax causes script errors
  • Example: sjust packages "node*" would match nodejs, node@20, yarn, etc.

Fix: For simple substring matching, use safer glob-style matching:

if [[ -n "${filter_package}" ]] && [[ "${package}" != *"${filter_package}"* ]]; then
    continue
fi

Or escape the regex if you want to keep regex semantics:

escaped_filter=$(printf '%s\n' "${filter_package}" | sed 's/[.[\*^$()+?{|]/\\&/g')
if [[ ! "${package}" =~ ${escaped_filter} ]]; then

3. Code Duplication (📐 Maintainability)

Files: sjust/libs/list-packages.sh:49-59 and sjust/libs/list-packages.sh:61-71

Problem: The loops for cask_packages and homebrew_packages are identical except for the section name.

Impact: Any bug fix or enhancement needs to be applied twice, increasing maintenance burden and risk of inconsistency.

Fix: Extract into a helper function:

process_package_section() {
    local section_name="$1"
    local filter="$2"
    
    while IFS='|' read -r package category description url; do
        [[ -z "${package}" ]] && continue
        
        # Apply filter if provided
        if [[ -n "${filter}" ]] && [[ "${package}" != *"${filter}"* ]]; then
            continue
        fi
        
        all_rows+=("${package}|${category}|${description}|${url}")
    done < <(get_yaml_package_entries "${PACKAGES_YML}" "${section_name}")
}

# Usage:
process_package_section "cask_packages" "${filter_package}"
process_package_section "homebrew_packages" "${filter_package}"

📝 Minor Issues

4. Typo in YAML

File: config/packages/all-packages.yml:16

description: ApplitE - Third-party app installer for macOS

Should be:

description: Applite - Third-party app installer for macOS

5. Outdated Comment in Tests

File: .github/workflows/test-sjust.yml:146

Comment says "3 columns" but the table now has 4 columns (Package, Category, Description, URL):

# Verify table format (3 columns: Package, Category, Description)

Should be:

# Verify table format (4 columns: Package, Category, Description, URL)

🎯 Existing Automated Review

The copilot-pull-request-reviewer already caught all these issues in its review. The suggestions provided are valid and should be implemented.


📊 Testing Recommendation

To verify the getline bug, run:

sjust packages | grep -E "(bc|btop|watch|openssl)"

Expected behavior: Should show all 4 packages
Current behavior: Likely shows only bc and watch (skipping btop and openssl@3)


💡 Suggested Action Plan

  1. Fix the critical getline bug in list-packages.sh (lines 32-36)
  2. Fix the unsafe regex in filter matching (lines 54, 66)
  3. Refactor duplicate loops into a helper function
  4. Fix the typo ("ApplitE" → "Applite")
  5. Update the outdated comment in the test workflow
  6. Test thoroughly to verify all packages appear in the output

Final Thoughts

This is a valuable feature that significantly improves the user experience. The design decisions are sound, and the implementation quality is generally high. Once the bugs are fixed, this will be ready to merge. The most critical issue is the getline bug causing packages to disappear—that needs to be addressed first.

Great work on the comprehensive metadata addition and the clean integration with sjust! The feature will be very useful once these issues are resolved.


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.

Have a way to catalog the installed packages from sparkdock divided by topic

2 participants