seedproject-web/api/commands/commands.md
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

2.9 KiB

CLI Commands — commands/

Scripts run via php console <CommandName>. No HTTP context — output goes to stdout.

File Naming

PascalCaseCommand.php — e.g. SyncDataCommand.php

Running Commands

php console SyncDataCommand
php console SyncDataCommand --date="2024-10-28" --force
php console CleanupDatabaseCommand --days=30 --dry-run
php console ProcessEmailQueueCommand --limit=50

Command Structure

<?php
// commands/SyncDataCommand.php

echo "=== Data Sync ===\n";

$options  = getopt("", ["date:", "force"]);
$date     = $options['date'] ?? 'today';
$force    = isset($options['force']);

echo "Syncing for: $date\n";

try {
    $records = Db::select("SELECT * FROM sp_sync_queue WHERE synced_at IS NULL");

    foreach ($records as $record) {
        if (!$force) {
            $exists = Db::getRow("SELECT 1 FROM sp_data WHERE external_id = :id LIMIT 1",
                [':id' => $record['external_id']]
            );
            if ($exists) { echo "Skipping {$record['external_id']}\n"; continue; }
        }

        Db::insert('sp_data', [
            'external_id' => $record['external_id'],
            'payload'     => $record['payload'],
            'synced_at'   => date('Y-m-d H:i:s')
        ]);
        echo "Inserted {$record['external_id']}\n";
    }

    echo "Done.\n";

} catch (Exception $e) {
    echo "ERROR: " . $e->getMessage() . "\n";
    exit(1);
}

Error Handling Pattern

Log individual failures and keep processing — exit with code 1 only on fatal errors:

$errors    = [];
$processed = 0;

foreach ($items as $item) {
    try {
        processItem($item);
        $processed++;
        echo ".";
    } catch (Exception $e) {
        $errors[] = ['id' => $item['id'], 'error' => $e->getMessage()];
        echo "E";
    }
}

echo "\n\nProcessed: $processed | Errors: " . count($errors) . "\n";

if (!empty($errors)) {
    foreach ($errors as $err) {
        echo "Item #{$err['id']}: {$err['error']}\n";
    }
    exit(1);
}

Dry-Run Pattern

$dryRun = isset($options['dry-run']);
if ($dryRun) { echo "DRY RUN — no changes will be made\n"; }

$count = Db::getRow("SELECT COUNT(*) as n FROM sp_logs WHERE created_at < :date",
    [':date' => $cutoffDate]
);
echo "Rows to delete: {$count['n']}\n";

if (!$dryRun) {
    Db::delete('sp_logs', 'created_at < :date', [':date' => $cutoffDate]);
}

Cron Setup

crontab -e
# Every 5 minutes
*/5 * * * * cd /www/wwwroot/appSeedProject && php console ProcessEmailQueueCommand >> /var/log/email-queue.log 2>&1

# Every hour
0 * * * * cd /www/wwwroot/appSeedProject && php console SyncDataCommand >> /var/log/sync.log 2>&1

# Nightly cleanup at 2 AM
0 2 * * * cd /www/wwwroot/appSeedProject && php console CleanupDatabaseCommand --days=30 >> /var/log/cleanup.log 2>&1