alwayscurious/laravel-project-devtool

Simplify your developer's startup time on new projects from existing repo.

Maintainers

Package info

github.com/AlwaysCuriousCo/laravel-project-devtool

Homepage

pkg:composer/alwayscurious/laravel-project-devtool

Fund package maintenance!

codearachnid

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-03 20:40 UTC

This package is auto-updated.

Last update: 2026-06-03 20:43:05 UTC


README

Latest Version on Packagist Tests Total Downloads

Simplify your developers' startup time on new projects from an existing repo — one command to a clean-slate dev environment, and a clean way for your app to hook into it.

php artisan project:dev --setup tears the local environment down and rebuilds it from scratch: clear caches → migrate:fresh → seed → build assets. Onboard a new teammate, recover from a broken branch, or reset between feature spikes in a single command instead of a wiki page of steps.

The part that makes it worth installing instead of writing your own shell script: at every stage of the reset it fires a lifecycle event, and your app attaches its own work by dropping a listener into app/Listeners. Generate permissions, seed demo data, print login credentials — without ever editing this package. The engine is mechanism; your app supplies the policy.

⚠️ Dev-only. --setup runs migrate:fresh, which drops every table. Install it as a --dev dependency and never point it at data you care about.

Install

composer require --dev alwayscurious/laravel-project-devtool

That's the whole setup — the command auto-registers. Publish the config only if you want to change defaults:

php artisan vendor:publish --tag="project-devtool-config"

Try it:

php artisan project:dev            # prints available actions, does nothing destructive
php artisan project:dev --setup    # the full reset, with a confirmation prompt

The command

project:dev
    {--setup            : Fresh reset — rebuild DB, seed, and build assets}
    {--new              : With --setup, also install dependencies (composer + npm install)}
    {--force            : With --setup, skip confirmation prompts}
    {--dry-run          : With --setup, simulate the run without making any changes}
    {--only=            : With --setup, run ONLY these steps (comma-separated)}
    {--skip=            : With --setup, skip these steps (comma-separated)}
    {--force-production : Allow --setup to run when the app environment is production}
You want to… Run
See what's available (safe, no-op) php artisan project:dev
Reset the environment php artisan project:dev --setup
Reset a freshly cloned repo, deps and all php artisan project:dev --setup --new
Reset unattended (CI, scripts) php artisan project:dev --setup --force
Preview exactly what would happen, change nothing php artisan project:dev --setup --dry-run
Just reseed (no wipe, no asset build) php artisan project:dev --setup --only=seed
Everything except the slow asset build php artisan project:dev --setup --skip=build

Before wiping anything, --setup confirms with a prompt that names the exact connection and database it's about to drop — so a wrong .env can't surprise you. --force skips it. And it refuses to run in production unless you pass --force-production.

Run only the steps you need

--setup is no longer all-or-nothing. The selectable steps, in order, are install, caches, migrate, seed, build. Use --only to run a subset or --skip to drop a few:

php artisan project:dev --setup --only=migrate,seed   # rebuild + reseed, skip caches/build
php artisan project:dev --setup --skip=build          # everything but the asset build

The confirmation prompt only appears when a destructive step (migrate or seed) is actually in the run — --only=caches won't ask.

Preview with --dry-run

--dry-run walks the entire sequence, prints what each step would do, fires every lifecycle event (so your listeners can announce their intent too), and changes nothing — no migrations, no seeding, no processes, no prompt:

DRY RUN — simulating; no changes will be made.

  [dry-run] would run: optimize:clear
  [dry-run] would run: migrate:fresh
  [dry-run] would run: db:seed
  [dry-run] would run: asset build

✔ Dry run complete — no changes were made.

Timing report

Every real run prints a per-step timing table at the end, so you can see where onboarding time actually goes:

 ------------------ ---------
  Step               Time
 ------------------ ---------
  optimize:clear     0.04s
  migrate:fresh      2.11s
  db:seed            0.83s
  asset build       14.29s
  total             17.27s
 ------------------ ---------

How --setup runs (and where you plug in)

The sequence is fixed and ordering-sensitive — your listeners can rely on it:

  ┌─ SetupStarting      ← guard point; a listener may veto here (AbortSetup)
  │   (wipe confirmation)
  ├─ optimize:clear  →  CachesCleared
  ├─ migrate:fresh   →  DatabaseMigrated   ← the gap before seeding
  ├─ db:seed         →  DatabaseSeeded
  ├─ asset build     →  AssetsBuilding (fired just before the build)
  └─ SetupCompleted    ← end-of-run report point

Each event carries the running command, so a listener gets the live console and can run nested artisan commands:

$event->command->info('');                                 // same output stream
$event->command->option('new');                             // read invocation flags
$event->command->call('some:command', ['--force' => true]); // nested artisan
$event->dryRun;                                             // true during --dry-run

Why seeding comes after a separate DatabaseMigrated event

This is the design detail the package exists for. Some apps must generate data (permissions, lookup tables) after the schema exists but before the seeder runs — because the seeder consumes it (e.g. granting permissions to a role). So the reset deliberately fires DatabaseMigrated in the gap between migrate:fresh and db:seed, giving you a hook at exactly that moment. Merge the two and the seam disappears; keep them split and your app slots right in.

Integrating your app: write a hook

A hook is just a listener that type-hints one of the lifecycle events. Laravel's event discovery wires it up — no registration, no config, no service provider edits.

Scaffold one

php artisan make:dev-hook BuildDemoData --event=DatabaseSeeded

Generates app/Listeners/BuildDemoData.php, pre-typed to the event and documented with the full event list. --event is validated and defaults to DatabaseSeeded.

Or write it by hand

namespace App\Listeners;

use AlwaysCurious\LaravelProjectDevtool\Events\DatabaseSeeded;

class BuildDemoData
{
    public function handle(DatabaseSeeded $event): void
    {
        // Respect a simulation: announce intent, change nothing.
        if ($event->dryRun) {
            $event->command->line('  [dry-run] would seed demo content');

            return;
        }

        $event->command->info('Seeding demo content…');
        $event->command->call('db:seed', [
            '--class' => \Database\Seeders\DemoSeeder::class,
            '--force' => true,
        ]);
    }
}

Lifecycle reference

Event Fires… Reach for it to…
SetupStarting before the wipe confirmation; may throw AbortSetup guard the run (env present? right branch?)
CachesCleared after optimize:clear prep that needs a clean cache/config
DatabaseMigrated after migrate:fresh, before seeding generate permissions / data the seeder needs
DatabaseSeeded after db:seed demo / sample data
AssetsBuilding just before the asset build prepare build inputs
SetupCompleted after the build, before the command returns print login URLs, credentials, next steps

Hooks can't break the reset

A thrown exception from any listener is reported and swallowed so a broken custom hook never leaves you with a half-built database. The single exception is the deliberate veto below.

Guarding the reset: AbortSetup

Veto a doomed run before anything destructive happens by throwing AbortSetup from a SetupStarting listener:

namespace App\Listeners;

use AlwaysCurious\LaravelProjectDevtool\Events\AbortSetup;
use AlwaysCurious\LaravelProjectDevtool\Events\SetupStarting;

class EnsureSuperAdminPassword
{
    public function handle(SetupStarting $event): void
    {
        if (empty(env('SUPER_ADMIN_PASSWORD'))) {
            throw new AbortSetup('SUPER_ADMIN_PASSWORD is not set — refusing to reset.');
        }
    }
}

The command prints the message, says “Nothing was changed.”, and exits with a failure code — the database is never touched. AbortSetup is honoured only from the SetupStarting pre-flight point; thrown later it's treated as a misplaced veto (warned and swallowed) so it can't corrupt a half-built database.

Batteries (opt-in recipes)

Common integrations ship as listeners that are off by default — a project that doesn't use them pulls in zero coupling — and self-guard when an optional dependency is missing.

Filament Shield permissions

GenerateShieldPermissions regenerates filament-shield permissions on DatabaseMigrated, in the gap before seeding, so your seeder can grant them to a super-admin role. If filament-shield isn't installed it skips with a notice instead of failing. Enable it in config:

'recipes' => [
    'shield' => [
        'enabled' => true,
        'panel'   => 'admin',
    ],
],

Configuration

config/project-devtool.php — everything you'd want to vary, nothing hardcoded:

return [
    // Seeder class run during --setup (null = framework default DatabaseSeeder).
    'seeder' => null,

    // Asset build command (argv array). Set to null/[] to skip the build step.
    'build' => ['npm', 'run', 'build'],

    // Dependency install commands used by --new.
    'install' => [
        'composer' => ['composer', 'install'],
        'npm'      => ['npm', 'install'],
    ],

    // Opt-in recipes — off by default.
    'recipes' => [
        'shield' => ['enabled' => false, 'panel' => 'admin'],
    ],
];
  • buildnull/[] skips the asset build (the command tells you it did).
  • seeder → a class name to seed something other than the default DatabaseSeeder.
  • install → swap in yarn, pnpm, bun, etc.

Requirements

  • PHP ^8.3
  • Laravel 11, 12, or 13

Testing

composer test

License

The MIT License (MIT). See License File.