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 @@ +