Installation & Setup Guide

Complete guide for installing, configuring, and getting started with MPM - Mehr's Package Manager.


Installation

Quick Install

# Download mpm.php
wget https://raw.githubusercontent.com/mehrnet/mpm/main/mpm.php

# Set permissions
chmod 644 mpm.php

# Test installation
php mpm.php echo "Hello, World!"

Manual Install

  1. Download mpm.php from the repository
  2. Place it in your project root or web server directory
  3. Ensure PHP 7.0+ is installed
  4. Verify ZipArchive extension is enabled (for package management)

Requirements

  • PHP: 7.0 or higher
  • Extensions: ZipArchive (for package management)
  • Permissions: Read/write access to project directory

First Run

On first execution, MPM automatically generates configuration files:

# Run any command to initialize
php mpm.php ls

This creates:

.config/
├── key              # API key (64 hex characters)
├── repos.json       # Repository mirrors configuration
├── packages.json    # Installed packages registry
└── path.json        # Command handler patterns

API Key

Your API key is displayed on first HTTP access or saved to .config/key on first CLI run:

# View your API key
cat .config/key

Important: Keep your API key secure. Anyone with access can execute commands.

Configuration

Repository Mirrors

Edit .config/repos.json to customize package repositories:

{
  "main": [
    "https://raw.githubusercontent.com/mehrnet/mpm-repo/refs/heads/main/main"
  ]
}

Each entry is a full URL to a repository directory containing database.json and package ZIP files. Mirrors are tried sequentially on failure.

Command Handler Patterns

Edit .config/path.json to customize where the shell looks for custom commands:

[
  "app/packages/[name]/handler.php",
  "bin/[name].php",
  "custom/handlers/[name].php"
]

The [name] placeholder is replaced with the command name.

Environment-Specific Configuration

For different environments, you can:

  1. Use different config directories:

    // Modify constants in mpm.php (not recommended)
    const DIR_CONFIG = '.config';
    
  2. Symlink configuration:

    ln -s .config.production .config
    
  3. Environment variables: Access via php mpm.php env list

Getting Started

# List files
php mpm.php ls

# Read file contents
php mpm.php cat mpm.php | head -20

# Create directory
php mpm.php mkdir tmp

# Copy files
php mpm.php cp mpm.php shell.backup.php

# Package management
php mpm.php pkg search database
php mpm.php pkg add users
php mpm.php pkg list
php mpm.php pkg upgrade

HTTP Mode (Production)

  1. Start PHP development server (testing):

    php -S localhost:8000
    
  2. Get your API key:

    KEY=$(cat .config/key)
    
  3. Execute commands:

    # List files
    curl "http://localhost:8000/mpm.php/$KEY/ls"
    
    # Read file
    curl "http://localhost:8000/mpm.php/$KEY/cat/README.md"
    
    # Package management
    curl "http://localhost:8000/mpm.php/$KEY/pkg/list"
    curl "http://localhost:8000/mpm.php/$KEY/pkg/add/users"
    

Production Web Server

For production, use a proper web server:

Nginx:

server {
    listen 80;
    server_name your-domain.com;
    root /var/www/shell;
    index mpm.php;

    location / {
        try_files $uri $uri/ /mpm.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index mpm.php;
        include fastcgi_params;
    }
}

Apache (.htaccess):

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ mpm.php/$1 [L,QSA]

Package Development

Creating a Package

  1. Create package structure:

    mypackage/
    ├── handler.php       # Required: Entry point
    ├── package.json      # Required: Metadata
    ├── lib/             # Optional: Library code
    └── data/            # Optional: Data files
    
  2. Write handler.php:

    <?php
    return function(array $args): string {
        $action = $args[0] ?? 'help';
    
        switch ($action) {
            case 'list':
                return "Item 1\nItem 2\nItem 3";
    
            case 'create':
                $name = $args[1] ?? null;
                if (!$name) {
                    throw new \RuntimeException('Name required', 400);
                }
                return "Created: $name";
    
            default:
                return "Actions: list, create";
        }
    };
    
  3. Create package.json:

    {
        "id": "mypackage",
        "name": "My Package",
        "version": "1.0.0",
        "description": "Package description",
        "author": "Your Name",
        "license": "MIT",
        "dependencies": []
    }
    

Package Metadata Fields

Field Type Required Description
id string Yes Lowercase, no spaces, used in URLs
name string Yes Human-readable name
version string Yes Semantic versioning (X.Y.Z)
description string Yes Brief description
author string No Package author
license string No License identifier (MIT, Apache-2.0, etc.)
dependencies array No Array of package IDs

Testing Your Package

CLI Mode:

# Install manually
mkdir -p app/packages/mypackage
cp -r mypackage/* app/packages/mypackage/

# Test commands
php mpm.php mypackage list
php mpm.php mypackage create item-name

HTTP Mode:

KEY=$(cat .config/key)
curl "http://localhost:8000/mpm.php/$KEY/mypackage/list"
curl "http://localhost:8000/mpm.php/$KEY/mypackage/create/item-name"

Best Practices

  1. Input Validation:

    $id = $args[0] ?? null;
    if (!$id || !is_numeric($id)) {
        throw new \RuntimeException('Invalid ID', 400);
    }
    
  2. Error Handling:

    try {
        $data = json_decode(file_get_contents('data.json'), true);
        if (!$data) {
            throw new \RuntimeException('Invalid JSON', 400);
        }
    } catch (\Throwable $e) {
        throw new \RuntimeException("Error: {$e->getMessage()}", 400);
    }
    
  3. Security:

    // BAD: Direct use of user input
    $file = $args[0];
    return file_get_contents($file);  // Vulnerable to path traversal
    
    // GOOD: Validate and restrict paths
    $file = basename($args[0] ?? '');  // Remove path components
    $path = "app/data/$file";
    if (!file_exists($path)) {
        throw new \RuntimeException('File not found', 404);
    }
    return file_get_contents($path);
    

Repository Setup

Creating a Package Repository

  1. Create repository structure:

    my-repo/
    └── main/
        ├── database.json
        ├── mypackage-1.0.0.zip
        └── otherapkg-1.0.0.zip
    
  2. Build package ZIP:

    cd mypackage
    zip -r ../my-repo/main/mypackage-1.0.0.zip .
    
  3. Calculate checksum:

    sha256sum my-repo/main/mypackage-1.0.0.zip
    # Output: abc123def456...  mypackage-1.0.0.zip
    
  4. Create database.json:

    {
      "version": 1,
      "packages": {
        "mypackage": {
          "name": "My Package",
          "description": "Package description",
          "versions": {
            "1.0.0": {
              "version": "1.0.0",
              "dependencies": [],
              "checksum": "sha256:abc123def456...",
              "size": 2048,
              "download_url": "mypackage-1.0.0.zip",
              "released_at": "2025-01-15"
            }
          },
          "latest": "1.0.0",
          "author": "Your Name",
          "license": "MIT"
        }
      }
    }
    
  5. Host the repository:

    • Upload to GitHub: https://github.com/user/repo/raw/main/main/
    • Use CDN or static hosting
    • Ensure HTTPS is enabled
  6. Configure shell to use your repository: Edit .config/repos.json:

    {
      "main": ["https://github.com/user/repo/raw/main/main"]
    }
    

GitHub Hosting Example

# Create repository
mkdir -p my-packages/main
cd my-packages

# Add packages
cp /path/to/mypackage-1.0.0.zip main/
cat > main/database.json << 'EOF'
{
  "version": 1,
  "packages": { ... }
}
EOF

# Push to GitHub
git init
git add .
git commit -m "Add packages"
git remote add origin https://github.com/user/my-packages.git
git push -u origin main

# Users configure: https://github.com/user/my-packages/raw/main/main

Deployment

Development

# Run PHP built-in server
php -S localhost:8000

# Test commands
php mpm.php pkg list
curl "http://localhost:8000/mpm.php/$(cat .config/key)/pkg/list"

Staging/Production

  1. Use proper web server (Nginx, Apache, Caddy)

  2. Enable HTTPS for API key security

  3. Set restrictive permissions:

    chmod 644 mpm.php
    chmod 700 .config
    chmod 600 .config/key
    
  4. Configure PHP-FPM for better performance

  5. Set up monitoring and logging

  6. Regular backups of .config/ directory

Docker Deployment

FROM php:8.1-fpm

# Install extensions
RUN docker-php-ext-install zip

# Copy shell
COPY mpm.php /var/www/html/

# Set permissions
RUN chown -R www-data:www-data /var/www/html

WORKDIR /var/www/html

Security Hardening

  1. Restrict API key access:

    chmod 600 .config/key
    chown www-data:www-data .config/key
    
  2. Use environment-specific keys

  3. Enable HTTPS only in production

  4. Rate limiting at web server level

  5. Monitor failed auth attempts

  6. Regular security updates

Troubleshooting

Common Issues

"Permission denied" errors:

# Fix permissions
chmod 755 .
chmod 644 mpm.php
chmod -R 755 .config

"ZipArchive not found":

# Install extension
sudo apt-get install php-zip  # Debian/Ubuntu
sudo yum install php-zip       # CentOS/RHEL

"API key not found" (CLI mode):

# Generate key manually
php mpm.php ls  # This creates .config/key

Lock file issues:

# Force unlock if stuck
php mpm.php pkg unlock

Usage Reference

Complete command reference and API documentation for MPM - Mehr's Package Manager.


Request Formats

HTTP Mode

Format:

GET /mpm.php/API_KEY/COMMAND/ARG0/ARG1/ARG2/...

Examples:

# Get API key
KEY=$(cat .config/key)

# List files
curl "http://localhost/mpm.php/$KEY/ls"

# Read file
curl "http://localhost/mpm.php/$KEY/cat/README.md"

# Echo with multiple arguments
curl "http://localhost/mpm.php/$KEY/echo/hello/world"

# Package management
curl "http://localhost/mpm.php/$KEY/pkg/search/database"
curl "http://localhost/mpm.php/$KEY/pkg/add/users"

Response:

  • Success: HTTP 200 with plain text response
  • Error: HTTP 4xx/5xx with error message

CLI Mode

Format:

php mpm.php COMMAND ARG0 ARG1 ARG2 ...

Examples:

# List files
php mpm.php ls

# Read file
php mpm.php cat README.md

# Echo with multiple arguments
php mpm.php echo hello world

# Package management
php mpm.php pkg search database
php mpm.php pkg add users

Response:

  • Success: Output to STDOUT, exit code 0
  • Error: Output to STDERR with "Error: " prefix, exit code 1

Built-in Commands

ls [path]

List directory contents.

Arguments:

  • path (optional): Directory to list (default: current directory)

Examples:

# CLI
php mpm.php ls
php mpm.php ls app
php mpm.php ls app/packages

# HTTP
curl "http://localhost/mpm.php/$KEY/ls"
curl "http://localhost/mpm.php/$KEY/ls/app"

Output: Space-separated filenames (excludes . and ..)

Errors:

  • 404: Directory not found
  • 400: Cannot read directory
  • 403: Path validation failed (absolute path or ..)

cat

Read file contents.

Arguments:

  • file (required): Path to file

Examples:

# CLI
php mpm.php cat README.md
php mpm.php cat .config/key

# HTTP
curl "http://localhost/mpm.php/$KEY/cat/README.md"

Output: File contents (preserves formatting and newlines)

Errors:

  • 400: Missing file argument or not a file
  • 404: File not found
  • 403: Path validation failed

rm

Delete file.

Arguments:

  • file (required): Path to file

Examples:

# CLI
php mpm.php rm temp.txt
php mpm.php rm logs/old.log

# HTTP
curl "http://localhost/mpm.php/$KEY/rm/temp.txt"

Output: Empty string on success (POSIX behavior)

Errors:

  • 400: Missing file argument, not a file, or cannot delete
  • 404: File not found
  • 403: Path validation failed

mkdir

Create directory (recursive).

Arguments:

  • path (required): Directory path to create

Examples:

# CLI
php mpm.php mkdir uploads
php mpm.php mkdir app/data/cache

# HTTP
curl "http://localhost/mpm.php/$KEY/mkdir/uploads"

Output: Empty string on success

Errors:

  • 400: Missing path argument, already exists, or cannot create
  • 403: Path validation failed

cp

Copy file.

Arguments:

  • src (required): Source file path
  • dst (required): Destination file path

Examples:

# CLI
php mpm.php cp config.json config.backup.json
php mpm.php cp README.md docs/README.md

# HTTP
curl "http://localhost/mpm.php/$KEY/cp/config.json/config.backup.json"

Output: Empty string on success

Errors:

  • 400: Missing arguments, source not a file, or cannot copy
  • 404: Source file not found
  • 403: Path validation failed

echo ...

Echo text arguments.

Arguments:

  • text... (required): One or more text arguments

Examples:

# CLI
php mpm.php echo hello
php mpm.php echo hello world "from shell"

# HTTP
curl "http://localhost/mpm.php/$KEY/echo/hello"
curl "http://localhost/mpm.php/$KEY/echo/hello/world"

Output: Space-separated arguments


env [action] [name]

Manage environment variables.

Actions:

  • list (default): List all environment variables
  • get <name>: Get specific variable value

Examples:

# CLI
php mpm.php env list
php mpm.php env get PATH
php mpm.php env get HOME

# HTTP
curl "http://localhost/mpm.php/$KEY/env/list"
curl "http://localhost/mpm.php/$KEY/env/get/PATH"

Output:

  • list: One variable per line (KEY=value)
  • get: Variable value only

Errors:

  • 404: Variable not found or unknown action
  • 400: Missing variable name for get

Package Manager

pkg add [PACKAGE...]

Install packages with automatic dependency resolution.

Arguments:

  • PACKAGE... (required): One or more package names

Examples:

# CLI
php mpm.php pkg add users
php mpm.php pkg add users auth database

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/add/users"
curl "http://localhost/mpm.php/$KEY/pkg/add/users/auth"

Process:

  1. Fetch repository database
  2. Resolve dependencies (DFS topological sort)
  3. Download all packages with checksum verification
  4. Extract all packages to project root
  5. Register packages atomically

Output:

Packages to install: dependency1, dependency2, users

Downloading packages...
All packages downloaded and verified

Extracting packages...
Extracted dependency1 (1.0.0)
Extracted dependency2 (2.0.0)
Extracted users (3.0.0)

Registering packages...

Successfully installed 3 package(s): dependency1, dependency2, users

Errors:

  • 404: Package not found
  • 400: Circular dependency detected
  • 503: All mirrors failed or lock held

pkg del

Remove package (fails if other packages depend on it).

Arguments:

  • PACKAGE (required): Package name

Examples:

# CLI
php mpm.php pkg del users

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/del/users"

Output:

Removed package: users (version 1.0.0) - 15 files deleted, 3 empty directories removed

Errors:

  • 404: Package not installed
  • 400: Package required by other packages
  • 503: Lock held

pkg upgrade [PACKAGE]

Upgrade all packages or specific package to latest version.

Arguments:

  • PACKAGE (optional): Package name (if omitted, upgrades all)

Examples:

# CLI
php mpm.php pkg upgrade           # Upgrade all
php mpm.php pkg upgrade users     # Upgrade specific

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/upgrade"
curl "http://localhost/mpm.php/$KEY/pkg/upgrade/users"

Output:

Will upgrade users from 1.0.0 to 1.1.0

Downloading package updates...
All package updates downloaded and verified

Extracting package updates...
Extracted users upgrade to 1.1.0

Registering upgrades...

Successfully upgraded 1 package(s): users

pkg update

Refresh repository cache (fetch latest package database).

Examples:

# CLI
php mpm.php pkg update

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/update"

Output:

Repository cache refreshed - 42 packages available

pkg list [FILTER]

List installed packages.

Arguments:

  • FILTER (optional): Filter by package name (case-insensitive)

Examples:

# CLI
php mpm.php pkg list
php mpm.php pkg list auth

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/list"
curl "http://localhost/mpm.php/$KEY/pkg/list/auth"

Output:

Installed packages:

  users                 v1.0.0      installed: 2025-01-15
  auth                  v2.0.0      installed: 2025-01-15 (depends: users)
  database              v1.5.0      installed: 2025-01-15 (depends: users)

pkg search

Search available packages.

Arguments:

  • KEYWORD (required): Search term

Examples:

# CLI
php mpm.php pkg search database

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/search/database"

Output:

Found 3 packages:

  mysql-driver          v1.0.0      MySQL Database Driver [installed]
    Provides MySQL connectivity for applications

  postgres-driver       v1.0.0      PostgreSQL Driver
    PostgreSQL database connector

  database-tools        v2.0.0      Database Management Tools
    Common database utilities and helpers

pkg info

Show detailed package information.

Arguments:

  • PACKAGE (required): Package name

Examples:

# CLI
php mpm.php pkg info users

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/info/users"

Output:

Package: User Management System
ID: users
Latest: 1.0.0
Description: Complete user management with authentication
Author: Mehrnet Team
License: MIT

Installed: v1.0.0 (on 2025-01-15T10:30:00Z)
Dependencies: none

Available versions:
  v1.0.0 - released: 2025-01-15
  v0.9.0 - released: 2025-01-01 (requires: auth-lib)

Errors:

  • 404: Package not found in repository

pkg unlock

Force remove lock file (for manual recovery).

Examples:

# CLI
php mpm.php pkg unlock

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/unlock"

Output: Empty string on success

When to use: If a package operation crashes, the lock file may remain. This command removes it.


pkg help

Show package manager help.

Examples:

# CLI
php mpm.php pkg help

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/help"

pkg version

Show package manager version.

Examples:

# CLI
php mpm.php pkg version

# HTTP
curl "http://localhost/mpm.php/$KEY/pkg/version"

Response Protocol

HTTP Status Codes

Code Meaning
200 Success
400 Bad Request (invalid arguments, operational errors)
403 Forbidden (invalid API key, path validation failed)
404 Not Found (command, package, or file not found)
503 Service Unavailable (all mirrors failed, lock held)

CLI Exit Codes

Code Meaning
0 Success
1 Error (any type)

POSIX Semantics

  • Commands with output return data
  • Commands without output return empty string (but still exit 0/200)
  • Errors throw exceptions with appropriate codes

Configuration Files

.config/key

Plain text file containing 64-character hex API key.

Format:

abc123def456789...  (64 hex characters)

Generation:

$key = bin2hex(random_bytes(32));

Security:

  • Auto-generated on first run
  • Used for HTTP authentication
  • Auto-loaded for CLI mode
  • Timing-safe comparison via hash_equals()

.config/repos.json

Repository mirrors configuration.

Format:

{
  "main": [
    "https://primary-mirror.com/packages",
    "https://secondary-mirror.com/packages"
  ],
  "extra": [
    "https://extra-repo.com/packages"
  ]
}

Mirror Selection:

  • Mirrors tried sequentially in array order
  • No backoff delay on failure
  • First successful mirror wins

.config/packages.json

Installed packages registry.

Format:

{
  "createdAt": "2025-01-15T10:00:00Z",
  "updatedAt": "2025-01-15T10:30:00Z",
  "packages": {
    "users": {
      "version": "1.0.0",
      "installed_at": "2025-01-15T10:30:00Z",
      "dependencies": [],
      "files": [
        "./app/packages/users/handler.php",
        "./app/packages/users/module.php"
      ],
      "download_url": "https://mirror.com/users-1.0.0.zip",
      "download_time": "2025-01-15T10:29:00Z",
      "checksum": "sha256:abc123...",
      "repository": "main"
    }
  }
}

Metadata:

  • createdAt: Registry creation time
  • updatedAt: Last modification time
  • files: Array of installed file paths (for removal)
  • download_url: Source mirror used
  • checksum: Verified checksum

.config/path.json

Command handler discovery patterns.

Format:

[
  "app/packages/[name]/handler.php",
  "bin/[name].php"
]

Pattern Matching:

  • [name] replaced with command name
  • Patterns searched in array order
  • First match wins

Example: Command users searches:

  1. app/packages/users/handler.php
  2. bin/users.php

Custom Commands

Handler Format

Handlers are PHP files returning a callable:

<?php
return function(array $args): string {
    // Process arguments
    // Return response or throw exception
    return "output";
};

Argument Processing

HTTP Mode:

GET /mpm.php/KEY/mycommand/action/arg1/arg2
                      ↓        ↓      ↓    ↓
                  command   args[0] [1]  [2]

CLI Mode:

php mpm.php mycommand action arg1 arg2
               ↓        ↓      ↓    ↓
           command   args[0] [1]  [2]

Handler receives:

function(array $args): string {
    $action = $args[0];  // 'action'
    $arg1 = $args[1];    // 'arg1'
    $arg2 = $args[2];    // 'arg2'
}

Error Handling

Throw exceptions with HTTP status codes:

// Bad request
throw new \RuntimeException('Invalid input', 400);

// Not found
throw new \RuntimeException('Item not found', 404);

// Forbidden
throw new \RuntimeException('Permission denied', 403);

Note: CLI mode converts all error codes to exit code 1.

Example Handler

<?php
return function(array $args): string {
    $action = $args[0] ?? 'help';

    switch ($action) {
        case 'list':
            // Read data
            $data = json_decode(
                file_get_contents('app/data/items.json'),
                true
            );
            return json_encode($data);

        case 'get':
            $id = $args[1] ?? null;
            if (!$id) {
                throw new \RuntimeException('ID required', 400);
            }
            // Fetch item
            return "Item: $id";

        case 'create':
            $name = $args[1] ?? null;
            if (!$name) {
                throw new \RuntimeException('Name required', 400);
            }
            // Create item
            return "Created: $name";

        case 'help':
            return "Actions: list, get <id>, create <name>";

        default:
            throw new \RuntimeException("Unknown action: $action", 404);
    }
};

Architecture

File Structure

.
├── mpm.php              # Main executable (2300 lines)
├── .config/               # Configuration (auto-created)
│   ├── key                # API key
│   ├── repos.json         # Repository mirrors
│   ├── packages.json      # Installed packages
│   └── path.json          # Handler patterns
├── .cache/                # Cache (auto-created)
│   ├── mpm.lock           # Lock file
│   └── *.zip              # Downloaded packages
└── app/packages/          # Installed packages
    └── [package]/
        └── handler.php

Execution Flow

1. Runtime Detection (HTTP/CLI)
   ↓
2. Initialize Config Files
   ↓
3. Parse Request (PATH_INFO or argv)
   ↓
4. Validate API Key
   ↓
5. Execute Command
   ├─ Built-in Command
   ├─ Package Manager
   └─ Custom Handler
   ↓
6. Send Response (HTTP headers or STDOUT/STDERR)

Package Manager Flow

Installation:

1. Resolve Dependencies (DFS topological sort)
   ↓
2. Download All Packages (with checksum verification)
   ↓
3. Extract All Packages (with conflict resolution)
   ↓
4. Register in .config/packages.json (atomic)

Key Features:

  • Dependency Resolution: O(V + E) complexity
  • Circular Detection: O(1) set lookups
  • Checksum Verification: SHA256 with timing-safe comparison
  • Mirror Failover: Sequential retry, no backoff
  • Atomic Operations: All-or-nothing installations
  • Conflict Resolution: Existing files renamed with -2, -3, etc.

Security Model

  1. Authentication:

    • 64-character random API key
    • hash_equals() timing-safe comparison
    • Auto-loaded in CLI mode
  2. Path Validation:

    • Blocks absolute paths (/etc/passwd)
    • Blocks directory traversal (../../../)
    • Applied to all file operations
  3. Package Security:

    • SHA256 checksum verification
    • Zip traversal detection
    • HTTPS mirror enforcement

Performance

Benchmarks

  • Application execution: <1ms (measured via profiling)
  • Built-in server overhead: 1000ms+ (use proper web server in production)
  • Package download: Depends on network and mirror speed
  • Dependency resolution: O(V + E) where V = packages, E = dependencies

Optimization Tips

  1. Use production web server (Nginx, Apache)
  2. Enable PHP opcode cache (OPcache)
  3. Use CDN for package mirrors
  4. Minimize custom handler complexity
  5. Cache package database locally

Troubleshooting

"Invalid API key"

HTTP 403

Cause: Missing or incorrect API key

Solution:

# Check key
cat .config/key

# Regenerate key (delete and run again)
rm .config/key
php mpm.php ls

"API key not found" (CLI)

Exit code 1

Cause: .config/key file doesn't exist

Solution:

# Initialize shell
php mpm.php ls

"Command not found"

HTTP 404 / Exit code 1

Cause: Handler file not found

Solution:

# Check handler patterns
cat .config/path.json

# Verify handler exists
ls app/packages/[command]/handler.php
ls bin/[command].php

"Another pkg operation is in progress"

HTTP 503 / Exit code 1

Cause: Lock file exists

Solution:

# Wait for operation to complete, or force unlock
php mpm.php pkg unlock

# Or manually remove
rm .cache/mpm.lock

"Package not found"

HTTP 404 / Exit code 1

Cause: Package not in repository

Solution:

# Refresh repository cache
php mpm.php pkg update

# Search for package
php mpm.php pkg search keyword

"Circular dependency"

HTTP 400 / Exit code 1

Cause: Package dependency cycle (A → B → C → A)

Solution: Review package dependencies and remove the cycle. This is a package repository issue.


"Cannot remove - required by"

HTTP 400 / Exit code 1

Cause: Other packages depend on the target package

Solution:

# Remove dependent packages first
php mpm.php pkg del dependent-package
php mpm.php pkg del target-package

Permission Errors

Cause: File system permissions

Solution:

# Fix permissions
chmod 755 .
chmod 644 mpm.php
chmod 755 .config
chmod 644 .config/*
chmod 755 .cache

Package Development Guide

Complete guide for creating, testing, and distributing MPM (Mehr's Package Manager) packages.

Quick Start

Create a minimal package in 3 steps:

# 1. Create package structure
mkdir -p mypackage
cd mypackage

# 2. Create handler
cat > handler.php << 'EOF'
<?php
return function(array $args): string {
    return "Hello from mypackage!";
};
EOF

# 3. Create metadata
cat > package.json << 'EOF'
{
    "id": "mypackage",
    "name": "My Package",
    "version": "1.0.0",
    "description": "A simple package",
    "author": "Your Name",
    "license": "MIT",
    "dependencies": []
}
EOF

# Test it
cd ..
mkdir -p app/packages/mypackage
cp -r mypackage/* app/packages/mypackage/
php mpm.php mypackage

Package Structure

Required Files

mypackage/
├── handler.php       # REQUIRED: Entry point
└── package.json      # REQUIRED: Metadata

Optional Structure

mypackage/
├── handler.php       # Entry point (required)
├── package.json      # Metadata (required)
├── lib/             # Library code
│   ├── MyClass.php
│   └── helpers.php
├── data/            # Data files
│   └── config.json
├── views/           # Templates
│   └── template.html
└── README.md        # Package documentation

File Extraction

Packages extract to project root preserving paths:

Package contains:

handler.php
module.php
lib/Database.php
data/schema.json

Extracts to:

./handler.php
./module.php
./lib/Database.php
./data/schema.json

For Mehr framework modules, use:

app/packages/[name]/handler.php
app/packages/[name]/module.php

Handler Implementation

Basic Handler

<?php
return function(array $args): string {
    // Single action
    return "Hello, World!";
};

Multi-Action Handler

<?php
return function(array $args): string {
    $action = $args[0] ?? 'help';

    switch ($action) {
        case 'list':
            return handleList();

        case 'get':
            $id = $args[1] ?? null;
            if (!$id) {
                throw new \RuntimeException('ID required', 400);
            }
            return handleGet($id);

        case 'create':
            $name = $args[1] ?? null;
            if (!$name) {
                throw new \RuntimeException('Name required', 400);
            }
            return handleCreate($name);

        case 'delete':
            $id = $args[1] ?? null;
            if (!$id) {
                throw new \RuntimeException('ID required', 400);
            }
            return handleDelete($id);

        case 'help':
            return <<<'EOF'
Available actions:
  list              - List all items
  get <id>          - Get item by ID
  create <name>     - Create new item
  delete <id>       - Delete item
EOF;

        default:
            throw new \RuntimeException("Unknown action: $action", 404);
    }
};

function handleList(): string {
    return "item1\nitem2\nitem3";
}

function handleGet(string $id): string {
    // Load from file, database, etc.
    return "Item data for: $id";
}

function handleCreate(string $name): string {
    // Create and persist
    return "Created: $name";
}

function handleDelete(string $id): string {
    // Remove item
    return "Deleted: $id";
}

Argument Processing

Arguments are identical in both HTTP and CLI modes:

HTTP:

GET /mpm.php/KEY/users/create/alice/alice@example.com
                    ↓      ↓      ↓         ↓
                command args[0] args[1]  args[2]

CLI:

php mpm.php users create alice alice@example.com
               ↓      ↓      ↓         ↓
           command args[0] args[1]  args[2]

Handler:

function(array $args): string {
    $action = $args[0];    // 'create'
    $name = $args[1];      // 'alice'
    $email = $args[2];     // 'alice@example.com'
}

File Operations

Handlers execute in mpm.php directory. Use relative paths:

<?php
return function(array $args): string {
    // Read data file
    $data = json_decode(
        file_get_contents('app/data/items.json'),
        true
    );

    // Modify data
    $data['new_item'] = $args[0] ?? 'default';

    // Write back
    file_put_contents(
        'app/data/items.json',
        json_encode($data, JSON_PRETTY_PRINT)
    );

    return "Updated";
};

Error Handling

<?php
return function(array $args): string {
    $id = $args[0] ?? null;

    // Validation
    if (!$id) {
        throw new \RuntimeException('ID required', 400);
    }

    if (!is_numeric($id)) {
        throw new \RuntimeException('ID must be numeric', 400);
    }

    // File operations
    $file = "app/data/$id.json";
    if (!file_exists($file)) {
        throw new \RuntimeException('Item not found', 404);
    }

    try {
        $content = file_get_contents($file);
        $data = json_decode($content, true);

        if (!$data) {
            throw new \RuntimeException('Invalid JSON', 400);
        }

        return json_encode($data);
    } catch (\Throwable $e) {
        throw new \RuntimeException(
            "Failed to read item: {$e->getMessage()}",
            400
        );
    }
};

Response Formats

Plain Text:

return "item1\nitem2\nitem3";

JSON:

return json_encode([
    'status' => 'success',
    'items' => ['item1', 'item2', 'item3']
]);

Empty (POSIX):

return '';  // Success with no output

Package Metadata

package.json Format

{
    "id": "mypackage",
    "name": "My Package",
    "version": "1.0.0",
    "description": "Package description",
    "author": "Your Name",
    "license": "MIT",
    "dependencies": ["dependency1", "dependency2"]
}

Field Specifications

Field Type Required Description
id string Yes Lowercase, no spaces, used in URLs
name string Yes Human-readable name
version string Yes Semantic versioning (X.Y.Z)
description string Yes Brief description
author string No Package author
license string No License identifier (MIT, Apache-2.0, etc.)
dependencies array No Array of package IDs

Dependency Declaration

{
    "id": "auth",
    "dependencies": ["users", "session"]
}

The package manager:

  • Resolves transitive dependencies automatically
  • Detects circular dependencies
  • Installs in correct order (dependencies before dependents)
  • Skips already-installed packages

Testing Packages

Manual Testing (CLI)

# Install package manually
mkdir -p app/packages/mypackage
cp -r mypackage/* app/packages/mypackage/

# Test commands
php mpm.php mypackage
php mpm.php mypackage list
php mpm.php mypackage get 123
php mpm.php mypackage create "Test Item"

Manual Testing (HTTP)

# Start server
php -S localhost:8000

# Get API key
KEY=$(cat .config/key)

# Test commands
curl "http://localhost:8000/mpm.php/$KEY/mypackage"
curl "http://localhost:8000/mpm.php/$KEY/mypackage/list"
curl "http://localhost:8000/mpm.php/$KEY/mypackage/get/123"

Testing Checklist

  • All actions work correctly
  • Error handling works (invalid input)
  • Required arguments are validated
  • File operations succeed
  • Both CLI and HTTP modes work identically
  • Help action documents all commands
  • Dependencies are declared in package.json

Distribution

Creating Package ZIP

cd mypackage
zip -r ../mypackage-1.0.0.zip .

# Verify contents
unzip -l ../mypackage-1.0.0.zip

Calculate Checksum

sha256sum mypackage-1.0.0.zip
# Output: abc123def456...  mypackage-1.0.0.zip

Automated Build Scripts

For reusable packaging, create shell scripts that automate the build process. This is especially useful for packaging third-party assets like icons, fonts, or JavaScript libraries.

Basic structure:

#!/bin/sh
set -e

PKG="mypackage"
DES="Package description"
VER="1.0.0"

URL="https://example.com/source.zip"
OUT="${PKG}-${VER}.zip"
TMP="$(mktemp -d)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

cd "$TMP"
# Download and extract source
wget -q "$URL" -O src.zip
unzip -q src.zip

# Restructure for your format
mkdir -p "assets/category/${PKG}"
mv source-files/* "assets/category/${PKG}/"

# Create package zip
zip -rq "$OUT" assets

# Calculate metadata
CHECKSUM=$(sha256sum "$OUT" | cut -d' ' -f1)
SIZE=$(wc -c < "$OUT" | tr -d ' ')
RELEASED=$(date +%Y-%m-%d)

# Move to repository
mv "$OUT" "${SCRIPT_DIR}/../main/"

# Update database.json
DB="${SCRIPT_DIR}/../main/database.json"
jq --arg id "$PKG" \
   --arg name "$PKG" \
   --arg desc "$DES" \
   --arg ver "$VER" \
   --arg checksum "sha256:$CHECKSUM" \
   --argjson size "$SIZE" \
   --arg url "$OUT" \
   --arg released "$RELEASED" \
   '.packages[$id] = {
       name: $name,
       description: $desc,
       author: "mpm-bot",
       license: "MIT",
       versions: { ($ver): { version: $ver, dependencies: [], checksum: $checksum, size: $size, download_url: $url, released_at: $released } },
       latest: $ver
   }' "$DB" > "${DB}.tmp" && mv "${DB}.tmp" "$DB"

rm -rf "$TMP"
echo "[+] Built ${PKG} v${VER}"

Example: Icon package (heroicons)

#!/bin/sh
set -e

PKG="heroicons"
DES="A set of free MIT-licensed high-quality SVG icons for UI development."
VER="2.2.0"

URL="https://github.com/tailwindlabs/heroicons/archive/refs/tags/v${VER}.zip"
OUT="${PKG}-${VER}.zip"
TMP="$(mktemp -d)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

cd "$TMP"
wget -q "$URL" -O src.zip
unzip -q src.zip

mkdir -p "assets/icons/${PKG}"
mv "heroicons-${VER}/src/"* "assets/icons/${PKG}/"

zip -rq "$OUT" assets

CHECKSUM=$(sha256sum "$OUT" | cut -d' ' -f1)
SIZE=$(wc -c < "$OUT" | tr -d ' ')
RELEASED=$(date +%Y-%m-%d)

mv "$OUT" "${SCRIPT_DIR}/../main/"

DB="${SCRIPT_DIR}/../main/database.json"
jq --arg id "$PKG" --arg name "$PKG" --arg desc "$DES" --arg ver "$VER" \
   --arg checksum "sha256:$CHECKSUM" --argjson size "$SIZE" \
   --arg url "$OUT" --arg released "$RELEASED" \
   '.packages[$id] = {
       name: $name, description: $desc, author: "mpm-bot", license: "MIT",
       versions: { ($ver): { version: $ver, dependencies: [], checksum: $checksum, size: $size, download_url: $url, released_at: $released } },
       latest: $ver
   }' "$DB" > "${DB}.tmp" && mv "${DB}.tmp" "$DB"

rm -rf "$TMP"
echo "[+] Built ${PKG} v${VER}"

Example: JavaScript library (htmx)

#!/bin/sh
set -e

PKG="htmx"
DES="High power tools for HTML."
VER="2.0.4"

URL="https://unpkg.com/htmx.org@${VER}/dist/htmx.min.js"
OUT="${PKG}-${VER}.zip"
TMP="$(mktemp -d)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

cd "$TMP"
mkdir -p "assets/js"
wget -q "$URL" -O "assets/js/htmx.min.js"

zip -rq "$OUT" assets

CHECKSUM=$(sha256sum "$OUT" | cut -d' ' -f1)
SIZE=$(wc -c < "$OUT" | tr -d ' ')
RELEASED=$(date +%Y-%m-%d)

mv "$OUT" "${SCRIPT_DIR}/../main/"

DB="${SCRIPT_DIR}/../main/database.json"
jq --arg id "$PKG" --arg name "$PKG" --arg desc "$DES" --arg ver "$VER" \
   --arg checksum "sha256:$CHECKSUM" --argjson size "$SIZE" \
   --arg url "$OUT" --arg released "$RELEASED" \
   '.packages[$id] = {
       name: $name, description: $desc, author: "mpm-bot", license: "BSD-2-Clause",
       versions: { ($ver): { version: $ver, dependencies: [], checksum: $checksum, size: $size, download_url: $url, released_at: $released } },
       latest: $ver
   }' "$DB" > "${DB}.tmp" && mv "${DB}.tmp" "$DB"

rm -rf "$TMP"
echo "[+] Built ${PKG} v${VER}"

Repository structure:

mpm-repo/
├── scripts/
│   ├── heroicons.sh
│   ├── htmx.sh
│   ├── alpinejs.sh
│   └── ...
└── main/
    ├── database.json
    ├── heroicons-2.2.0.zip
    ├── htmx-2.0.4.zip
    └── ...

Build all packages:

for script in scripts/*.sh; do ./$script; done

Repository database.json

{
  "version": 1,
  "packages": {
    "mypackage": {
      "name": "My Package",
      "description": "Package description",
      "author": "Your Name",
      "license": "MIT",
      "versions": {
        "1.0.0": {
          "version": "1.0.0",
          "dependencies": [],
          "checksum": "sha256:abc123def456...",
          "size": 2048,
          "download_url": "mypackage-1.0.0.zip",
          "released_at": "2025-01-15"
        }
      },
      "latest": "1.0.0"
    }
  }
}

Files are tracked locally during extraction for clean uninstallation.

GitHub Hosting

# Create repository structure
mkdir -p my-packages/main
cd my-packages/main

# Add package and database
cp /path/to/mypackage-1.0.0.zip .
cat > database.json << 'EOF'
{
  "version": 1,
  "packages": { ... }
}
EOF

# Push to GitHub
cd ..
git init
git add .
git commit -m "Add mypackage"
git remote add origin https://github.com/user/my-packages.git
git push -u origin main

# Users configure:
# "main": ["https://github.com/user/my-packages/raw/main/main"]

Best Practices

Input Validation

// BAD: No validation
$id = $args[0];
return processId($id);

// GOOD: Validate and sanitize
$id = $args[0] ?? null;
if (!$id || !is_numeric($id) || $id < 1) {
    throw new \RuntimeException('Invalid ID', 400);
}
return processId((int)$id);

Security

// BAD: Arbitrary file access
$file = $args[0];
return file_get_contents($file);  // Vulnerable to ../../../etc/passwd

// GOOD: Restrict to safe directory
$file = basename($args[0] ?? '');  // Remove path components
$path = "app/data/$file";

if (!file_exists($path)) {
    throw new \RuntimeException('File not found', 404);
}

return file_get_contents($path);

Error Messages

// BAD: Generic error
throw new \RuntimeException('Error', 400);

// GOOD: Descriptive error
throw new \RuntimeException('User ID must be a positive integer', 400);

Code Organization

// BAD: Everything in handler
return function(array $args): string {
    // 500 lines of code...
};

// GOOD: Use lib/ directory
return function(array $args): string {
    require_once __DIR__ . '/lib/Manager.php';
    $manager = new Manager();
    return $manager->handle($args);
};

Dependencies

// GOOD: Check if Composer libraries available
if (file_exists(__DIR__ . '/../../vendor/autoload.php')) {
    require_once __DIR__ . '/../../vendor/autoload.php';
    // Use libraries
} else {
    throw new \RuntimeException('Composer dependencies missing', 500);
}

Examples

Simple Counter

<?php
return function(array $args): string {
    $file = 'app/data/counter.txt';

    if (!file_exists($file)) {
        file_put_contents($file, '0');
    }

    $count = (int)file_get_contents($file);
    $count++;
    file_put_contents($file, (string)$count);

    return "Count: $count";
};

JSON CRUD

<?php
return function(array $args): string {
    $action = $args[0] ?? 'list';
    $file = 'app/data/items.json';

    // Load data
    $items = [];
    if (file_exists($file)) {
        $items = json_decode(file_get_contents($file), true) ?? [];
    }

    switch ($action) {
        case 'list':
            return json_encode($items);

        case 'add':
            $name = $args[1] ?? null;
            if (!$name) {
                throw new \RuntimeException('Name required', 400);
            }
            $items[] = [
                'id' => count($items) + 1,
                'name' => $name,
                'created_at' => date('Y-m-d H:i:s')
            ];
            file_put_contents($file, json_encode($items, JSON_PRETTY_PRINT));
            return "Added: $name";

        case 'get':
            $id = (int)($args[1] ?? 0);
            foreach ($items as $item) {
                if ($item['id'] === $id) {
                    return json_encode($item);
                }
            }
            throw new \RuntimeException('Item not found', 404);

        default:
            throw new \RuntimeException('Unknown action', 404);
    }
};

Mehr Framework Module

mymodule/
├── app/
│   └── packages/
│       └── mymodule/
│           ├── handler.php       # Shell command handler
│           ├── module.php        # Module implementation
│           ├── manifest.json     # Mehr manifest
│           └── migrations/       # Database migrations
│               └── 001_create_table.php
└── package.json                  # Distribution metadata

handler.php:

<?php
return function(array $args): string {
    require_once __DIR__ . '/module.php';
    $module = new MyModule();
    return $module->handleCommand($args);
};

package.json:

{
    "id": "mymodule",
    "name": "My Module",
    "version": "1.0.0",
    "description": "Mehr framework module",
    "dependencies": ["core"]
}