PHPackages                             alwayscurious/laravel-project-devtool - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. alwayscurious/laravel-project-devtool

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

alwayscurious/laravel-project-devtool
=====================================

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

v1.0.0(1mo ago)05↓90%[1 PRs](https://github.com/AlwaysCuriousCo/laravel-project-devtool/pulls)MITPHPPHP ^8.3CI passing

Since Jun 3Pushed 3w agoCompare

[ Source](https://github.com/AlwaysCuriousCo/laravel-project-devtool)[ Packagist](https://packagist.org/packages/alwayscurious/laravel-project-devtool)[ Docs](https://github.com/alwayscurious/laravel-project-devtool)[ GitHub Sponsors](https://github.com/codearachnid)[ RSS](/packages/alwayscurious-laravel-project-devtool/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (12)Versions (5)Used By (0)

Laravel Project Devtool
=======================

[](#laravel-project-devtool)

[![Latest Version on Packagist](https://camo.githubusercontent.com/2a4109837475102d5d52156395c042cb67c20c5d89b754502048d6d471b5ca29/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f616c77617973637572696f75732f6c61726176656c2d70726f6a6563742d646576746f6f6c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/alwayscurious/laravel-project-devtool)[![Tests](https://github.com/alwayscurious/laravel-project-devtool/actions/workflows/run-tests.yml/badge.svg)](https://github.com/alwayscurious/laravel-project-devtool/actions/workflows/run-tests.yml)[![Total Downloads](https://camo.githubusercontent.com/c264d53f8727ac7cb9807f75b1cfe64124d905d4e7c58b88950a59b536266a39/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f616c77617973637572696f75732f6c61726176656c2d70726f6a6563742d646576746f6f6c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/alwayscurious/laravel-project-devtool)

**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
-------

[](#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
-----------

[](#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…RunSee 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

[](#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`

[](#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

[](#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)
------------------------------------------

[](#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

[](#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
----------------------------------

[](#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

[](#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

[](#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

[](#lifecycle-reference)

EventFires…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** seedinggenerate permissions / data the seeder needs`DatabaseSeeded`after `db:seed`demo / sample data`AssetsBuilding`just before the asset buildprepare build inputs`SetupCompleted`after the build, before the command returnsprint login URLs, credentials, next steps### Hooks can't break the reset

[](#hooks-cant-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`
--------------------------------

[](#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)
--------------------------

[](#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

[](#filament-shield-permissions)

`GenerateShieldPermissions` regenerates [filament-shield](https://github.com/bezhanSalleh/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
-------------

[](#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'],
    ],
];
```

- `build` → `null`/`[]` 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
------------

[](#requirements)

- PHP `^8.3`
- Laravel 11, 12, or 13

Testing
-------

[](#testing)

```
composer test
```

License
-------

[](#license)

The MIT License (MIT). See [License File](LICENSE.md).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance92

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

51d ago

Major Versions

v0.0.1 → v1.0.02026-06-03

PHP version history (2 changes)v0.0.1PHP ^8.4

v1.0.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/fa0676f3242298155f63579f1f3823e6cc441cb651e35aa4423494a30ccf22ce?d=identicon)[codearachnid](/maintainers/codearachnid)

---

Top Contributors

[![codearachnid](https://avatars.githubusercontent.com/u/755025?v=4)](https://github.com/codearachnid "codearachnid (14 commits)")

---

Tags

laravelAlways Curious Colaravel-project-devtool

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/alwayscurious-laravel-project-devtool/health.svg)

```
[![Health](https://phpackages.com/badges/alwayscurious-laravel-project-devtool/health.svg)](https://phpackages.com/packages/alwayscurious-laravel-project-devtool)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M48](/packages/spatie-laravel-pdf)[codewithdennis/filament-select-tree

The multi-level select field enables you to make single selections from a predefined list of options that are organized into multiple levels or depths.

330530.5k30](/packages/codewithdennis-filament-select-tree)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
