From 6dc8d5f410c4bdaf27abacff1a80de8df11f5689 Mon Sep 17 00:00:00 2001 From: Flint Date: Tue, 7 Jul 2026 06:18:59 -0400 Subject: [PATCH 1/4] Add AI agent readiness: course sub-commands, docs, PHP 7.4 (#31) - New `wp llms course content ` command for course structure - New `wp llms course enrollments ` command for enrollment listing - AI agent usage guide (docs/ai-agents.md) with patterns for Claude Code, Cursor, Codex - README rewrite with installation, quick start, command reference, output formats - Bump minimum PHP to 7.4 (7.3 EOL Nov 2021) Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 15 +++ README.md | 154 +++++++++++++++++++++-- composer.json | 2 +- docs/ai-agents.md | 184 ++++++++++++++++++++++++++++ src/Commands/Course/Content.php | 120 ++++++++++++++++++ src/Commands/Course/Enrollments.php | 130 ++++++++++++++++++++ src/Commands/Course/Main.php | 28 +++++ src/commands.php | 10 ++ 8 files changed, 632 insertions(+), 11 deletions(-) create mode 100644 docs/ai-agents.md create mode 100644 src/Commands/Course/Content.php create mode 100644 src/Commands/Course/Enrollments.php create mode 100644 src/Commands/Course/Main.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 957c979..5a04203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ LifterLMS CLI Changelog ======================= +v0.0.6 - 2026-03-30 +------------------- + +##### New Features + ++ Added `wp llms course content ` command to retrieve course structure (sections and lessons) in a single call. ++ Added `wp llms course enrollments ` command to list students enrolled in a specific course. ++ Added AI agent usage guide (`docs/ai-agents.md`) with patterns for Claude Code, Cursor, Codex, and similar tools. + +##### Updates + ++ Rewrote README with installation guide, quick start examples, command reference, output format documentation, and AI agent usage section. ++ Updated minimum PHP version to 7.4 (7.3 reached EOL November 2021). + + v0.0.5 - 2025-01-21 ------------------- diff --git a/README.md b/README.md index 6f77c67..7846dff 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -LLMS-CLI -======== +LifterLMS CLI +============= [![Test PHPUnit][img-gh-testing]][link-gh-testing] [![GitHub Coding Standards Workflow Status][img-gh-cs]][link-gh-cs] @@ -8,24 +8,159 @@ LLMS-CLI --- -The LLMS-CLI is a collection of WP-CLI commands for [LifterLMS](https://github.com/gocodebox/lifterlms). +WP-CLI commands for [LifterLMS](https://github.com/gocodebox/lifterlms). Manage courses, memberships, enrollments, students, and more from the command line. This is a feature plugin which will be included in the LifterLMS core plugin automatically. --- -## Documentation +## Installation + +Install as a WP-CLI package: + +```bash +wp package install gocodebox/lifterlms-cli +``` + +Or clone into your `wp-content/plugins` directory: + +```bash +cd wp-content/plugins +git clone https://github.com/gocodebox/lifterlms-cli.git +``` + +**Requirements:** +- PHP 7.4+ +- WordPress 5.0+ +- [LifterLMS](https://lifterlms.com) 5.0+ +- [WP-CLI](https://wp-cli.org/) 2.x + +## Quick Start + +```bash +# List all courses +wp llms course list + +# Get a specific course +wp llms course get 42 + +# Create a course +wp llms course create --title="Introduction to Python" --status=draft + +# Get course structure (sections + lessons) +wp llms course content 42 + +# List enrolled students +wp llms course enrollments 42 + +# Enroll a student in a course +wp llms students-enrollments create --student_id=5 --post_id=42 + +# Check student progress +wp llms students-progress get 5 --post_id=42 +``` + +## Commands + +### Resource Commands + +All resource commands support `list`, `get`, `create`, `update`, `delete`, `diff`, `edit`, and `generate` subcommands. + +| Command | Description | +|---------|-------------| +| `wp llms course` | Manage courses | +| `wp llms section` | Manage sections | +| `wp llms lesson` | Manage lessons | +| `wp llms membership` | Manage memberships | +| `wp llms access-plan` | Manage access plans (pricing) | +| `wp llms student` | Manage students | +| `wp llms instructor` | Manage instructors | +| `wp llms students-enrollments` | Manage student enrollments | +| `wp llms students-progress` | Manage student progress | +| `wp llms api-key` | Manage REST API keys | + +### Course Sub-Resource Commands + +| Command | Description | +|---------|-------------| +| `wp llms course content ` | Get course structure (sections + lessons) | +| `wp llms course enrollments ` | List students enrolled in a course | -Documentation is automatically generated and imported from the [docs/](./docs) directory into the developer hub at [developer.lifterlms.com/cli/commands](https://developer.lifterlms.com/cli/commands/). +### Management Commands +| Command | Description | +|---------|-------------| +| `wp llms addon` | Manage LifterLMS add-ons (requires LifterLMS Helper) | +| `wp llms license` | Manage add-on licenses (requires LifterLMS Helper) | +| `wp llms version` | Display LifterLMS version | -## Installing for development +## Output Formats -To install for development either: +All commands support multiple output formats via `--format`: -+ Download the [latest release](https://github.com/gocodebox/lifterlms-cli/releases) and upload to your WordPress site via FTP or add as new plugin. -+ Clone this repository into your `wp-content/plugins` directory. +```bash +# Default table format +wp llms course list +# JSON (recommended for scripts and AI agents) +wp llms course list --format=json + +# CSV +wp llms course list --format=csv + +# Just IDs +wp llms course list --format=ids + +# YAML +wp llms course list --format=yaml + +# Count +wp llms course list --format=count +``` + +Limit output to specific fields: + +```bash +wp llms course list --fields=id,title,status --format=json +``` + +Get just the ID after creating/updating: + +```bash +wp llms course create --title="My Course" --porcelain +# Returns: 42 +``` + +## Using with AI Agents + +The LifterLMS CLI works with AI coding assistants like Claude Code, Cursor, and Codex. See the [AI Agent Guide](docs/ai-agents.md) for detailed patterns and examples. + +Key tips: +- Always use `--format=json` for structured, parseable output +- Use `--fields` to reduce response size +- Use `--porcelain` on create/update to get just the new ID +- Chain commands with pipes: `wp llms course list --format=ids | xargs -I{} wp llms course get {} --format=json` + +## Remote Sites + +Use [WP-CLI aliases](https://make.wordpress.org/cli/handbook/guides/running-commands-remotely/) to manage remote sites: + +```yaml +# ~/.wp-cli/config.yml +@staging: + ssh: user@staging.example.com/var/www/html +@production: + ssh: user@example.com/var/www/html +``` + +```bash +wp @staging llms course list +wp @production llms student list --format=json +``` + +## Documentation + +Full command reference is available at [developer.lifterlms.com/cli/commands](https://developer.lifterlms.com/cli/commands/) and in the [docs/](./docs) directory. ## Contributing @@ -33,7 +168,6 @@ Please follow the contribution guidelines put forth by the [LifterLMS core](http - [img-cc-coverage]:https://img.shields.io/codeclimate/coverage/gocodebox/lifterlms-cli?style=for-the-badge&logo=code-climate [img-cc-maintainability]:https://img.shields.io/codeclimate/maintainability/gocodebox/lifterlms-cli?logo=code-climate&style=for-the-badge [img-gh-testing]:https://img.shields.io/github/workflow/status/gocodebox/lifterlms-cli/Test%20PHPUnit?label=tests&logo=github&style=for-the-badge diff --git a/composer.json b/composer.json index 4b4b734..9e3c4b4 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } }, "require": { - "php": ">=7.3" + "php": ">=7.4" }, "archive": { "exclude": [ diff --git a/docs/ai-agents.md b/docs/ai-agents.md new file mode 100644 index 0000000..e30ebe5 --- /dev/null +++ b/docs/ai-agents.md @@ -0,0 +1,184 @@ +# Using LifterLMS CLI with AI Agents + +This guide covers how to use the LifterLMS CLI effectively with AI coding assistants like Claude Code, Cursor, Codex, and similar tools. + +## Why CLI for AI Agents? + +AI agents that have shell access (Claude Code, Codex, Cursor terminal) can use the LifterLMS CLI directly — no MCP server or API configuration needed. The CLI runs inside WordPress, so it has full access to all LifterLMS data with proper permission handling. + +## Essential Patterns + +### Always Use JSON Output + +AI agents parse structured data, not ASCII tables. Always pass `--format=json`: + +```bash +# Bad: returns an ASCII table that's hard to parse +wp llms course list + +# Good: returns structured JSON +wp llms course list --format=json +``` + +### Limit Fields to Reduce Context Size + +LLM context windows are finite. Use `--fields` to request only what you need: + +```bash +# All fields (verbose) +wp llms course get 42 --format=json + +# Just what you need +wp llms course get 42 --fields=id,title,status --format=json +``` + +### Use --porcelain for Create/Update Workflows + +When creating or updating resources, `--porcelain` returns just the ID — useful for chaining: + +```bash +# Create a course and capture its ID +COURSE_ID=$(wp llms course create --title="My Course" --status=draft --porcelain) + +# Create a section in that course +SECTION_ID=$(wp llms section create --title="Getting Started" --parent_id=$COURSE_ID --porcelain) + +# Create a lesson in that section +wp llms lesson create --title="Welcome" --parent_id=$SECTION_ID --status=publish --porcelain +``` + +### Get Course Structure in One Call + +Use `wp llms course content` to see the full outline: + +```bash +wp llms course content 42 --format=json +``` + +Returns sections with their lessons, ordered by position. This is faster than listing sections and lessons separately. + +### Check Enrollment Status + +```bash +# How many students in a course? +wp llms course enrollments 42 --format=count + +# Who's enrolled? +wp llms course enrollments 42 --format=json + +# Is student 5 enrolled in course 42? +wp llms students-enrollments list --student_id=5 --post_id=42 --format=json +``` + +## Common Workflows + +### Create a Complete Course + +```bash +# 1. Create the course +COURSE_ID=$(wp llms course create --title="Photography Basics" --status=draft --porcelain) + +# 2. Create sections +S1=$(wp llms section create --title="Camera Fundamentals" --parent_id=$COURSE_ID --order=1 --porcelain) +S2=$(wp llms section create --title="Composition" --parent_id=$COURSE_ID --order=2 --porcelain) + +# 3. Create lessons +wp llms lesson create --title="Aperture & Shutter Speed" --parent_id=$S1 --order=1 --status=publish --porcelain +wp llms lesson create --title="ISO & Exposure" --parent_id=$S1 --order=2 --status=publish --porcelain +wp llms lesson create --title="Rule of Thirds" --parent_id=$S2 --order=1 --status=publish --porcelain +wp llms lesson create --title="Leading Lines" --parent_id=$S2 --order=2 --status=publish --porcelain + +# 4. Create an access plan +wp llms access-plan create --title="Full Access" --post_id=$COURSE_ID --price=49 --frequency=0 + +# 5. Publish +wp llms course update $COURSE_ID --status=publish + +# 6. Verify +wp llms course content $COURSE_ID --format=json +``` + +### Bulk Enrollment + +```bash +# Enroll multiple students in a course +for STUDENT_ID in 5 12 28 45; do + wp llms students-enrollments create --student_id=$STUDENT_ID --post_id=42 +done + +# Verify enrollments +wp llms course enrollments 42 --format=json +``` + +### Progress Report + +```bash +# Get all enrollments for a course +STUDENTS=$(wp llms course enrollments 42 --fields=student_id --format=json) + +# Check each student's progress +wp llms students-progress list --post_id=42 --format=json +``` + +### Find and Update + +```bash +# Find a course by searching +wp llms course list --search="Python" --fields=id,title --format=json + +# Update its title +wp llms course update 42 --title="Advanced Python Programming" +``` + +## Resource Relationships + +Understanding how LifterLMS resources relate to each other: + +``` +Course +├── Sections (ordered, belong to one course) +│ └── Lessons (ordered, belong to one section) +│ └── Quizzes (optional, attached to lessons) +├── Access Plans (pricing, one or more per course) +└── Enrollments (students enrolled in this course) + └── Progress (per-student, per-course/lesson) + +Membership +├── Access Plans (pricing) +├── Auto-enrollment courses (optional) +└── Enrollments (students) +``` + +Key IDs to track: +- **course_id** / **post_id**: The course or membership ID +- **student_id**: WordPress user ID +- **parent_id**: Parent course (for sections) or parent section (for lessons) + +## Error Handling + +CLI commands exit with standard exit codes: +- `0` = success +- `1` = error (with message on stderr) + +Error messages are human-readable. For AI agents, check the exit code: + +```bash +if wp llms course get 99999 --format=json 2>/dev/null; then + echo "Course exists" +else + echo "Course not found" +fi +``` + +## CLI vs MCP Server + +LifterLMS has both a CLI and an [MCP server](https://github.com/gocodebox/lifterlms-mcp). When to use which: + +| Use CLI when... | Use MCP when... | +|-----------------|-----------------| +| You have shell access to the WordPress server | You're connecting remotely | +| Running Claude Code, Codex, or Cursor locally | Using Claude Desktop or ChatGPT | +| Need to chain commands or write scripts | Want conversational CRUD | +| Working in CI/CD pipelines | Don't have SSH access | + +Both tools cover the same LifterLMS resources. The CLI talks to WordPress directly (internal REST requests). The MCP server talks over HTTP (external REST API with Application Passwords). diff --git a/src/Commands/Course/Content.php b/src/Commands/Course/Content.php new file mode 100644 index 0000000..5f31d1d --- /dev/null +++ b/src/Commands/Course/Content.php @@ -0,0 +1,120 @@ + + * : The course ID. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - json + * - csv + * - yaml + * --- + * + * [--fields=] + * : Limit the output to specific fields. + * + * ## EXAMPLES + * + * # Get course outline as a table. + * $ wp llms course content 123 + * + * # Get course outline as JSON (recommended for AI agents). + * $ wp llms course content 123 --format=json + * + * @since [version] + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of command options. + * @return void + */ + public function content( $args, $assoc_args ) { + + list( $course_id ) = $args; + $course_id = absint( $course_id ); + + if ( ! $course_id || ! get_post( $course_id ) || 'course' !== get_post_type( $course_id ) ) { + \WP_CLI::error( sprintf( 'Course %d not found.', $course_id ) ); + } + + if ( ! defined( 'REST_REQUEST' ) ) { + define( 'REST_REQUEST', true ); + } + + $request = new \WP_REST_Request( 'GET', "/llms/v1/courses/{$course_id}/content" ); + $response = rest_do_request( $request ); + + if ( $error = $response->as_error() ) { + \WP_CLI::error( $error ); + } + + $data = $response->get_data(); + $format = \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' ); + + if ( 'json' === $format ) { + echo wp_json_encode( $data, JSON_PRETTY_PRINT ); + return; + } + + if ( 'yaml' === $format ) { + echo \Spyc::YAMLDump( $data, false, false, true ); + return; + } + + // Flatten for table/csv display. + $items = array(); + foreach ( $data as $section ) { + $items[] = array( + 'type' => 'section', + 'id' => $section['id'] ?? '', + 'title' => $section['title']['rendered'] ?? $section['title'] ?? '', + 'order' => $section['order'] ?? '', + 'parent' => $course_id, + ); + + $lessons = $section['lessons'] ?? $section['content'] ?? array(); + foreach ( $lessons as $lesson ) { + $items[] = array( + 'type' => 'lesson', + 'id' => $lesson['id'] ?? '', + 'title' => $lesson['title']['rendered'] ?? $lesson['title'] ?? '', + 'order' => $lesson['order'] ?? '', + 'parent' => $section['id'] ?? '', + ); + } + } + + $fields = \WP_CLI\Utils\get_flag_value( $assoc_args, 'fields', 'type,id,title,order,parent' ); + $formatter = new \WP_CLI\Formatter( $assoc_args, explode( ',', $fields ) ); + $formatter->display_items( $items ); + } + +} diff --git a/src/Commands/Course/Enrollments.php b/src/Commands/Course/Enrollments.php new file mode 100644 index 0000000..12ad0f0 --- /dev/null +++ b/src/Commands/Course/Enrollments.php @@ -0,0 +1,130 @@ + + * : The course ID. + * + * [--page=] + * : Page number for paginated results. + * --- + * default: 1 + * --- + * + * [--per_page=] + * : Number of results per page. + * --- + * default: 10 + * --- + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - json + * - csv + * - yaml + * - count + * --- + * + * [--fields=] + * : Limit the output to specific fields. + * + * ## EXAMPLES + * + * # List enrollments for course 123. + * $ wp llms course enrollments 123 + * + * # Get enrollment count. + * $ wp llms course enrollments 123 --format=count + * + * # Get enrollments as JSON (recommended for AI agents). + * $ wp llms course enrollments 123 --format=json + * + * @since [version] + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of command options. + * @return void + */ + public function enrollments( $args, $assoc_args ) { + + list( $course_id ) = $args; + $course_id = absint( $course_id ); + + if ( ! $course_id || ! get_post( $course_id ) || 'course' !== get_post_type( $course_id ) ) { + \WP_CLI::error( sprintf( 'Course %d not found.', $course_id ) ); + } + + if ( ! defined( 'REST_REQUEST' ) ) { + define( 'REST_REQUEST', true ); + } + + $request = new \WP_REST_Request( 'GET', "/llms/v1/courses/{$course_id}/enrollments" ); + $request->set_param( 'page', $assoc_args['page'] ?? 1 ); + $request->set_param( 'per_page', $assoc_args['per_page'] ?? 10 ); + + $response = rest_do_request( $request ); + + if ( $error = $response->as_error() ) { + \WP_CLI::error( $error ); + } + + $data = $response->get_data(); + $headers = $response->get_headers(); + $format = \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' ); + + if ( 'count' === $format ) { + echo (int) ( $headers['X-WP-Total'] ?? count( $data ) ); + return; + } + + if ( 'json' === $format ) { + echo wp_json_encode( $data, JSON_PRETTY_PRINT ); + return; + } + + if ( 'yaml' === $format ) { + echo \Spyc::YAMLDump( $data, false, false, true ); + return; + } + + if ( empty( $data ) ) { + \WP_CLI::log( 'No enrollments found.' ); + return; + } + + $fields = \WP_CLI\Utils\get_flag_value( $assoc_args, 'fields', null ); + if ( $fields ) { + $fields = explode( ',', $fields ); + } else { + $fields = array_keys( $data[0] ); + } + + $formatter = new \WP_CLI\Formatter( $assoc_args, $fields ); + $formatter->display_items( $data ); + } + +} diff --git a/src/Commands/Course/Main.php b/src/Commands/Course/Main.php new file mode 100644 index 0000000..0fa3098 --- /dev/null +++ b/src/Commands/Course/Main.php @@ -0,0 +1,28 @@ + Date: Thu, 16 Jul 2026 10:06:14 -0400 Subject: [PATCH 2/4] Skip sub-resources routes, fix bug in list_items/get_item. --- .changelogs/dev.yml | 3 ++ src/Commands/Restful/Command.php | 34 +++++++++------------ src/Commands/Restful/Runner.php | 51 +++++++++++--------------------- 3 files changed, 34 insertions(+), 54 deletions(-) create mode 100644 .changelogs/dev.yml diff --git a/.changelogs/dev.yml b/.changelogs/dev.yml new file mode 100644 index 0000000..cd5161e --- /dev/null +++ b/.changelogs/dev.yml @@ -0,0 +1,3 @@ +significance: patch +type: fixed +entry: Skip sub-resources routes in discovery, fix bug in list_items/get_item. diff --git a/src/Commands/Restful/Command.php b/src/Commands/Restful/Command.php index c7eaa3a..4ecd9c1 100644 --- a/src/Commands/Restful/Command.php +++ b/src/Commands/Restful/Command.php @@ -101,12 +101,10 @@ public function delete_item( $args, $assoc_args ) { $id = isset( $body['previous'] ) ? $body['previous']['id'] : $body['id']; if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { \WP_CLI::line( $id ); - } else { - if ( empty( $assoc_args['force'] ) ) { + } elseif ( empty( $assoc_args['force'] ) ) { \WP_CLI::success( "Trashed {$this->name} {$id}." ); - } else { - \WP_CLI::success( "Deleted {$this->name} {$id}." ); - } + } else { + \WP_CLI::success( "Deleted {$this->name} {$id}." ); } } @@ -119,7 +117,7 @@ public function get_item( $args, $assoc_args ) { list( $status, $body, $headers ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); if ( ! empty( $assoc_args['fields'] ) ) { - $body = self::limit_item_to_fields( $body, $fields ); + $body = self::limit_item_to_fields( $body, $assoc_args['fields'] ); } if ( 'headers' === $assoc_args['format'] ) { @@ -161,7 +159,7 @@ public function list_items( $args, $assoc_args ) { if ( ! empty( $assoc_args['fields'] ) ) { foreach ( $items as $key => $item ) { - $items[ $key ] = self::limit_item_to_fields( $item, $fields ); + $items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] ); } } @@ -233,7 +231,7 @@ public function diff_items( $args, $assoc_args ) { if ( ! is_null( $resource ) ) { $field = is_numeric( $resource ) ? 'id' : 'slug'; - $callback = function( $value ) use ( $field, $resource ) { + $callback = function ( $value ) use ( $field, $resource ) { if ( isset( $value[ $field ] ) && $resource == $value[ $field ] ) { return true; } @@ -381,7 +379,7 @@ private function do_request( $method, $route, $assoc_args ) { } usort( $performed_queries, - function( $a, $b ) { + function ( $a, $b ) { if ( $a[1] === $b[1] ) { return 0; } @@ -398,7 +396,7 @@ function( $a, $b ) { if ( $performed_queries && 'rest' === \WP_CLI::get_config( 'debug' ) ) { $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL; foreach ( $performed_queries as $i => $query ) { - $i++; + ++$i; $bits = explode( ', ', $query[2] ); $backtrace = implode( ', ', array_slice( $bits, 13 ) ); $seconds = round( $query[1], 6 ); @@ -463,12 +461,10 @@ protected function get_formatter( &$assoc_args ) { } else { $fields = $assoc_args['fields']; } - } else { - if ( ! empty( $assoc_args['context'] ) ) { + } elseif ( ! empty( $assoc_args['context'] ) ) { $fields = $this->get_context_fields( $assoc_args['context'] ); - } else { - $fields = $this->get_context_fields( 'view' ); - } + } else { + $fields = $this->get_context_fields( 'view' ); } return new \WP_CLI\Formatter( $assoc_args, $fields ); } @@ -495,7 +491,7 @@ private function get_context_fields( $context ) { * @return string */ private function get_base_route() { - return substr( $this->route, 0, strlen( $this->route ) - strlen( $this->resource_identifier ) ); + return substr( $this->route, 0, strlen( $this->route ) - strlen( (string) $this->resource_identifier ) ); } /** @@ -522,7 +518,7 @@ private function show_difference( $slug, $difference ) { */ private function recursively_show_difference( $dictated, $current = null ) { - $this->output_nesting_level++; + ++$this->output_nesting_level; if ( $this->is_assoc_array( $dictated ) ) { @@ -584,8 +580,7 @@ private function recursively_show_difference( $dictated, $current = null ) { } } - $this->output_nesting_level--; - + --$this->output_nesting_level; } /** @@ -666,5 +661,4 @@ private static function limit_item_to_fields( $item, $fields ) { } return $item; } - } diff --git a/src/Commands/Restful/Runner.php b/src/Commands/Restful/Runner.php index 0eab5a3..e4a2480 100644 --- a/src/Commands/Restful/Runner.php +++ b/src/Commands/Restful/Runner.php @@ -57,6 +57,13 @@ public static function after_wp_load() { continue; } + // Skip sub-resource routes (a path segment after the resource ID, e.g. `/content`). + // These share their parent's schema title, so auto-discovering them clobbers the parent's + // list/get commands. They are registered explicitly instead (see Course\Main). + if ( preg_match( '#\)/[^/]+/?$#', $route ) ) { + continue; + } + if ( empty( $route_data['schema']['title'] ) ) { \WP_CLI::debug( "No schema title found for {$route}, skipping LifterLMS CLI REST command registration.", 'lifterlms' ); continue; @@ -67,24 +74,14 @@ public static function after_wp_load() { self::register_route_commands( $rest_command, $route, $route_data ); } - } - private static function get_command_root_desc( $resource ) { - $resource = str_replace( array( '-', 'students', 'api' ), array( ' ', 'student', 'API' ), $resource ); - if ( 's' !== substr( $resource, -1 ) ) { - $resource .= 's'; - } - return sprintf( 'Manage %s.', $resource ); - } - private static function get_command_short_desc( $command, $resource ) { $before = ''; $after = ''; - switch ( $command ) { case 'create': $before = 'Creates a new'; @@ -95,18 +92,18 @@ private static function get_command_short_desc( $command, $resource ) { break; case 'diff': - $before = 'Compare'; + $before = 'Compare'; $resource = self::pluralize_resource( $resource ); - $after = 'between environments'; + $after = 'between environments'; break; case 'edit': $before = 'Launches system editor to edit the'; - $after = 'content'; + $after = 'content'; break; case 'generate': - $before = 'Generates some'; + $before = 'Generates some'; $resource = self::pluralize_resource( $resource ); break; @@ -115,7 +112,7 @@ private static function get_command_short_desc( $command, $resource ) { break; case 'list': - $before = 'Gets a list of '; + $before = 'Gets a list of '; $resource = self::pluralize_resource( $resource ); break; @@ -179,7 +176,6 @@ private static function get_supported_commands( $route, $route_data ) { } return $supported_commands; - } public static function before_invoke_command() { @@ -200,7 +196,6 @@ public static function before_invoke_command() { if ( \WP_CLI::get_config( 'debug' ) && ! defined( 'SAVEQUERIES' ) ) { define( 'SAVEQUERIES', true ); } - } /** @@ -293,15 +288,6 @@ private static function register_route_commands( $rest_command, $route, $route_d 'update' => 'update_item', ); - // Add the root command, eg: wp llms course. - \WP_CLI::add_command( - "{$parent}", - $rest_command, - array( - 'shortdesc' => self::get_command_root_desc( $resource ), - ) - ); - // Register main subcommands, eg: wp llms course create, wp llms course delete, etc... \WP_CLI::add_command( "{$parent} {$command}", @@ -319,7 +305,7 @@ private static function register_route_commands( $rest_command, $route, $route_d "{$parent} diff", array( $rest_command, 'diff_items' ), array( - 'shortdesc' => self::get_command_short_desc( 'diff', $resource ), + 'shortdesc' => self::get_command_short_desc( 'diff', $resource ), 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), ) ); @@ -331,14 +317,13 @@ private static function register_route_commands( $rest_command, $route, $route_d "{$parent} generate", array( $rest_command, 'generate_items' ), array( - 'shortdesc' => self::get_command_short_desc( 'generate', $resource ), - 'synopsis' => self::get_generate_command_synopsis( $synopsis ), + 'shortdesc' => self::get_command_short_desc( 'generate', $resource ), + 'synopsis' => self::get_generate_command_synopsis( $synopsis ), 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), ) ); } - // If updating and getting is supported, add the edit command. if ( 'update' === $command && array_key_exists( 'get', $supported_commands ) ) { $synopsis = array(); @@ -352,8 +337,8 @@ private static function register_route_commands( $rest_command, $route, $route_d "{$parent} edit", array( $rest_command, 'edit_item' ), array( - 'shortdesc' => self::get_command_short_desc( 'edit', $resource ), - 'synopsis' => $synopsis, + 'shortdesc' => self::get_command_short_desc( 'edit', $resource ), + 'synopsis' => $synopsis, 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), ) ); @@ -385,7 +370,5 @@ private static function get_generate_command_synopsis( $create_synopsis ) { ); return array_merge( $generate_synopsis, $create_synopsis ); - } - } From 59cbd1ea0808c4edbbb519b2d3dc1ea3056a08f5 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 16 Jul 2026 10:26:38 -0400 Subject: [PATCH 3/4] Adding changelogs, removing from changelog.md. --- .changelogs/dev-1.yml | 4 ++++ .changelogs/dev-2.yml | 4 ++++ .changelogs/dev-3.yml | 4 ++++ .changelogs/dev-4.yml | 4 ++++ .changelogs/dev-5.yml | 3 +++ CHANGELOG.md | 14 -------------- 6 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 .changelogs/dev-1.yml create mode 100644 .changelogs/dev-2.yml create mode 100644 .changelogs/dev-3.yml create mode 100644 .changelogs/dev-4.yml create mode 100644 .changelogs/dev-5.yml diff --git a/.changelogs/dev-1.yml b/.changelogs/dev-1.yml new file mode 100644 index 0000000..b8ebc60 --- /dev/null +++ b/.changelogs/dev-1.yml @@ -0,0 +1,4 @@ +significance: patch +type: added +entry: Added `wp llms course content ` command to retrieve course structure + (sections and lessons) in a single call. diff --git a/.changelogs/dev-2.yml b/.changelogs/dev-2.yml new file mode 100644 index 0000000..936d0fa --- /dev/null +++ b/.changelogs/dev-2.yml @@ -0,0 +1,4 @@ +significance: patch +type: added +entry: Added `wp llms course enrollments ` command to list students enrolled + in a specific course. diff --git a/.changelogs/dev-3.yml b/.changelogs/dev-3.yml new file mode 100644 index 0000000..3ee9e0f --- /dev/null +++ b/.changelogs/dev-3.yml @@ -0,0 +1,4 @@ +significance: patch +type: added +entry: Added AI agent usage guide (`docs/ai-agents.md`) with patterns for Claude + Code, Cursor, Codex, and similar tools. diff --git a/.changelogs/dev-4.yml b/.changelogs/dev-4.yml new file mode 100644 index 0000000..0e7944c --- /dev/null +++ b/.changelogs/dev-4.yml @@ -0,0 +1,4 @@ +significance: patch +type: changed +entry: Rewrote README with installation guide, quick start examples, command + reference, output format documentation, and AI agent usage section. diff --git a/.changelogs/dev-5.yml b/.changelogs/dev-5.yml new file mode 100644 index 0000000..849d01d --- /dev/null +++ b/.changelogs/dev-5.yml @@ -0,0 +1,3 @@ +significance: patch +type: changed +entry: Updated minimum PHP version to 7.4 (7.3 reached EOL November 2021). diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a04203..9490509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,6 @@ LifterLMS CLI Changelog ======================= -v0.0.6 - 2026-03-30 -------------------- - -##### New Features - -+ Added `wp llms course content ` command to retrieve course structure (sections and lessons) in a single call. -+ Added `wp llms course enrollments ` command to list students enrolled in a specific course. -+ Added AI agent usage guide (`docs/ai-agents.md`) with patterns for Claude Code, Cursor, Codex, and similar tools. - -##### Updates - -+ Rewrote README with installation guide, quick start examples, command reference, output format documentation, and AI agent usage section. -+ Updated minimum PHP version to 7.4 (7.3 reached EOL November 2021). - v0.0.5 - 2025-01-21 ------------------- From bcf82a5450b9179d9c806b9183ab8d9f43414961 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 16 Jul 2026 10:27:37 -0400 Subject: [PATCH 4/4] Build for 0.0.6 --- .changelogs/dev-1.yml | 4 ---- .changelogs/dev-2.yml | 4 ---- .changelogs/dev-3.yml | 4 ---- .changelogs/dev-4.yml | 4 ---- .changelogs/dev-5.yml | 3 --- .changelogs/dev.yml | 3 --- CHANGELOG.md | 19 +++++++++++++++++++ lifterlms-cli.php | 2 +- package-lock.json | 2 +- package.json | 2 +- src/Commands/Course/Content.php | 8 ++++---- src/Commands/Course/Enrollments.php | 8 ++++---- src/Commands/Course/Main.php | 6 +++--- src/commands.php | 2 +- 14 files changed, 34 insertions(+), 37 deletions(-) delete mode 100644 .changelogs/dev-1.yml delete mode 100644 .changelogs/dev-2.yml delete mode 100644 .changelogs/dev-3.yml delete mode 100644 .changelogs/dev-4.yml delete mode 100644 .changelogs/dev-5.yml delete mode 100644 .changelogs/dev.yml diff --git a/.changelogs/dev-1.yml b/.changelogs/dev-1.yml deleted file mode 100644 index b8ebc60..0000000 --- a/.changelogs/dev-1.yml +++ /dev/null @@ -1,4 +0,0 @@ -significance: patch -type: added -entry: Added `wp llms course content ` command to retrieve course structure - (sections and lessons) in a single call. diff --git a/.changelogs/dev-2.yml b/.changelogs/dev-2.yml deleted file mode 100644 index 936d0fa..0000000 --- a/.changelogs/dev-2.yml +++ /dev/null @@ -1,4 +0,0 @@ -significance: patch -type: added -entry: Added `wp llms course enrollments ` command to list students enrolled - in a specific course. diff --git a/.changelogs/dev-3.yml b/.changelogs/dev-3.yml deleted file mode 100644 index 3ee9e0f..0000000 --- a/.changelogs/dev-3.yml +++ /dev/null @@ -1,4 +0,0 @@ -significance: patch -type: added -entry: Added AI agent usage guide (`docs/ai-agents.md`) with patterns for Claude - Code, Cursor, Codex, and similar tools. diff --git a/.changelogs/dev-4.yml b/.changelogs/dev-4.yml deleted file mode 100644 index 0e7944c..0000000 --- a/.changelogs/dev-4.yml +++ /dev/null @@ -1,4 +0,0 @@ -significance: patch -type: changed -entry: Rewrote README with installation guide, quick start examples, command - reference, output format documentation, and AI agent usage section. diff --git a/.changelogs/dev-5.yml b/.changelogs/dev-5.yml deleted file mode 100644 index 849d01d..0000000 --- a/.changelogs/dev-5.yml +++ /dev/null @@ -1,3 +0,0 @@ -significance: patch -type: changed -entry: Updated minimum PHP version to 7.4 (7.3 reached EOL November 2021). diff --git a/.changelogs/dev.yml b/.changelogs/dev.yml deleted file mode 100644 index cd5161e..0000000 --- a/.changelogs/dev.yml +++ /dev/null @@ -1,3 +0,0 @@ -significance: patch -type: fixed -entry: Skip sub-resources routes in discovery, fix bug in list_items/get_item. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9490509..8809a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ LifterLMS CLI Changelog ======================= +v0.0.6 - 2026-07-16 +------------------- + +##### New Features + ++ Added `wp llms course content ` command to retrieve course structure (sections and lessons) in a single call. ++ Added `wp llms course enrollments ` command to list students enrolled in a specific course. ++ Added AI agent usage guide (`docs/ai-agents.md`) with patterns for Claude Code, Cursor, Codex, and similar tools. + +##### Updates and Enhancements + ++ Rewrote README with installation guide, quick start examples, command reference, output format documentation, and AI agent usage section. ++ Updated minimum PHP version to 7.4 (7.3 reached EOL November 2021). + +##### Bug Fixes + ++ Skip sub-resources routes in discovery, fix bug in list_items/get_item. + + v0.0.5 - 2025-01-21 ------------------- diff --git a/lifterlms-cli.php b/lifterlms-cli.php index 5c2f81b..77982cf 100644 --- a/lifterlms-cli.php +++ b/lifterlms-cli.php @@ -10,7 +10,7 @@ * Plugin Name: LifterLMS CLI * Plugin URI: https://lifterlms.com/ * Description: WP CLI feature plugin for the LifterLMS Core. - * Version: 0.0.5 + * Version: 0.0.6 * Author: LifterLMS * Author URI: https://lifterlms.com/ * Text Domain: lifterlms diff --git a/package-lock.json b/package-lock.json index 597afb1..8fb7c6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lifterlms-cli", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 48ad9bb..f21fc32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lifterlms-cli", - "version": "0.0.5", + "version": "0.0.6", "description": "WP CLI commands for the LifterLMS plugin and add-ons.", "repository": { "type": "git", diff --git a/src/Commands/Course/Content.php b/src/Commands/Course/Content.php index 5f31d1d..61b8c00 100644 --- a/src/Commands/Course/Content.php +++ b/src/Commands/Course/Content.php @@ -4,8 +4,8 @@ * * @package LifterLMS/CLI * - * @since [version] - * @version [version] + * @since 0.0.6 + * @version 0.0.6 */ namespace LifterLMS\CLI\Commands\Course; @@ -13,7 +13,7 @@ /** * Course content command trait. * - * @since [version] + * @since 0.0.6 */ trait Content { @@ -50,7 +50,7 @@ trait Content { * # Get course outline as JSON (recommended for AI agents). * $ wp llms course content 123 --format=json * - * @since [version] + * @since 0.0.6 * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of command options. diff --git a/src/Commands/Course/Enrollments.php b/src/Commands/Course/Enrollments.php index 12ad0f0..39e2781 100644 --- a/src/Commands/Course/Enrollments.php +++ b/src/Commands/Course/Enrollments.php @@ -4,8 +4,8 @@ * * @package LifterLMS/CLI * - * @since [version] - * @version [version] + * @since 0.0.6 + * @version 0.0.6 */ namespace LifterLMS\CLI\Commands\Course; @@ -13,7 +13,7 @@ /** * Course enrollments command trait. * - * @since [version] + * @since 0.0.6 */ trait Enrollments { @@ -63,7 +63,7 @@ trait Enrollments { * # Get enrollments as JSON (recommended for AI agents). * $ wp llms course enrollments 123 --format=json * - * @since [version] + * @since 0.0.6 * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of command options. diff --git a/src/Commands/Course/Main.php b/src/Commands/Course/Main.php index 0fa3098..add0ac3 100644 --- a/src/Commands/Course/Main.php +++ b/src/Commands/Course/Main.php @@ -4,8 +4,8 @@ * * @package LifterLMS/CLI * - * @since [version] - * @version [version] + * @since 0.0.6 + * @version 0.0.6 */ namespace LifterLMS\CLI\Commands\Course; @@ -19,7 +19,7 @@ * by adding access to sub-resource REST API routes that the * Restful bridge does not discover automatically. * - * @since [version] + * @since 0.0.6 */ class Main extends AbstractCommand { diff --git a/src/commands.php b/src/commands.php index 8bccdfe..7d25de2 100644 --- a/src/commands.php +++ b/src/commands.php @@ -47,6 +47,6 @@ * Adds commands for course sub-resource endpoints (content, enrollments) * that are not auto-discovered by the Restful bridge. * - * @since [version] + * @since 0.0.6 */ WP_CLI::add_command( 'llms course', 'LifterLMS\CLI\Commands\Course\Main' );